@expo/ui 58.0.20 → 58.0.22

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 (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +86 -2
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/java/expo/modules/ui/CompositionLocalProviderView.kt +26 -0
  5. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +12 -0
  6. package/android/src/main/java/expo/modules/ui/HostView.kt +4 -1
  7. package/android/src/main/java/expo/modules/ui/MaskView.kt +3 -2
  8. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +3 -1
  9. package/android/src/main/java/expo/modules/ui/RNHostView.kt +11 -7
  10. package/android/src/main/java/expo/modules/ui/SwitchView.kt +6 -1
  11. package/android/src/main/java/expo/modules/ui/TopAppBarView.kt +62 -0
  12. package/android/src/main/java/expo/modules/ui/colors/MaterialColors.kt +61 -0
  13. package/build/community/bottom-sheet/BottomSheet.ios.js +8 -3
  14. package/build/community/bottom-sheet/BottomSheet.ios.js.map +1 -1
  15. package/build/community/pager-view/PagerView.ios.js +25 -8
  16. package/build/community/pager-view/PagerView.ios.js.map +1 -1
  17. package/build/community/segmented-control/SegmentedControl.ios.js +11 -2
  18. package/build/community/segmented-control/SegmentedControl.ios.js.map +1 -1
  19. package/build/community/segmented-control/types.d.ts +5 -1
  20. package/build/community/segmented-control/types.js.map +1 -1
  21. package/build/community/segmented-control/vendor/SegmentedControl.js +2 -2
  22. package/build/community/segmented-control/vendor/SegmentedControl.js.map +1 -1
  23. package/build/jetpack-compose/CompositionLocalProvider/index.d.ts +28 -0
  24. package/build/jetpack-compose/CompositionLocalProvider/index.js +15 -0
  25. package/build/jetpack-compose/CompositionLocalProvider/index.js.map +1 -0
  26. package/build/jetpack-compose/Host/index.d.ts +8 -0
  27. package/build/jetpack-compose/Host/index.js +38 -5
  28. package/build/jetpack-compose/Host/index.js.map +1 -1
  29. package/build/jetpack-compose/Switch/index.d.ts +2 -0
  30. package/build/jetpack-compose/Switch/index.js +1 -0
  31. package/build/jetpack-compose/Switch/index.js.map +1 -1
  32. package/build/jetpack-compose/TopAppBar/index.d.ts +72 -0
  33. package/build/jetpack-compose/TopAppBar/index.js +45 -0
  34. package/build/jetpack-compose/TopAppBar/index.js.map +1 -0
  35. package/build/jetpack-compose/index.d.ts +2 -0
  36. package/build/jetpack-compose/index.js +2 -0
  37. package/build/jetpack-compose/index.js.map +1 -1
  38. package/build/jetpack-compose/modifiers/index.d.ts +5 -1
  39. package/build/jetpack-compose/modifiers/index.js +6 -2
  40. package/build/jetpack-compose/modifiers/index.js.map +1 -1
  41. package/build/swift-ui/BottomSheet/index.d.ts +6 -0
  42. package/build/swift-ui/BottomSheet/index.js +9 -3
  43. package/build/swift-ui/BottomSheet/index.js.map +1 -1
  44. package/build/universal/Checkbox/index.android.js +16 -4
  45. package/build/universal/Checkbox/index.android.js.map +1 -1
  46. package/build/universal/ScrollView/index.ios.js +12 -6
  47. package/build/universal/ScrollView/index.ios.js.map +1 -1
  48. package/build/universal/Switch/index.android.js +17 -4
  49. package/build/universal/Switch/index.android.js.map +1 -1
  50. package/build/universal/Switch/index.js +1 -0
  51. package/build/universal/Switch/index.js.map +1 -1
  52. package/build/universal/Text/index.js +2 -2
  53. package/build/universal/Text/index.js.map +1 -1
  54. package/build/universal/webUtils.js +4 -0
  55. package/build/universal/webUtils.js.map +1 -1
  56. package/ios/BottomSheetView.swift +11 -2
  57. package/ios/DataListForEachView.swift +24 -6
  58. package/ios/DatePickerView.swift +134 -9
  59. package/ios/Modifiers/SelectedSegmentTintColorModifier.swift +86 -0
  60. package/ios/Modifiers/ViewModifierRegistry.swift +4 -0
  61. package/ios/RNHostView.swift +34 -16
  62. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.20/expo.modules.ui-58.0.20-sources.jar → 58.0.22/expo.modules.ui-58.0.22-sources.jar} +0 -0
  63. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22-sources.jar.md5 +1 -0
  64. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22-sources.jar.sha1 +1 -0
  65. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22-sources.jar.sha256 +1 -0
  66. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22-sources.jar.sha512 +1 -0
  67. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.aar +0 -0
  68. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.aar.md5 +1 -0
  69. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.aar.sha1 +1 -0
  70. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.aar.sha256 +1 -0
  71. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.aar.sha512 +1 -0
  72. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.20/expo.modules.ui-58.0.20.module → 58.0.22/expo.modules.ui-58.0.22.module} +22 -22
  73. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.module.md5 +1 -0
  74. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.module.sha1 +1 -0
  75. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.module.sha256 +1 -0
  76. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.module.sha512 +1 -0
  77. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{58.0.20/expo.modules.ui-58.0.20.pom → 58.0.22/expo.modules.ui-58.0.22.pom} +1 -1
  78. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.pom.md5 +1 -0
  79. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.pom.sha1 +1 -0
  80. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.pom.sha256 +1 -0
  81. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.22/expo.modules.ui-58.0.22.pom.sha512 +1 -0
  82. package/package.json +2 -2
  83. package/src/community/bottom-sheet/BottomSheet.ios.tsx +7 -3
  84. package/src/community/pager-view/PagerView.ios.tsx +25 -8
  85. package/src/community/segmented-control/SegmentedControl.ios.tsx +13 -1
  86. package/src/community/segmented-control/types.tsx +5 -1
  87. package/src/community/segmented-control/vendor/SegmentedControl.tsx +10 -2
  88. package/src/jetpack-compose/CompositionLocalProvider/index.tsx +43 -0
  89. package/src/jetpack-compose/Host/index.tsx +50 -4
  90. package/src/jetpack-compose/Switch/index.tsx +4 -0
  91. package/src/jetpack-compose/TopAppBar/index.tsx +113 -0
  92. package/src/jetpack-compose/index.ts +2 -0
  93. package/src/jetpack-compose/modifiers/index.ts +11 -3
  94. package/src/swift-ui/BottomSheet/index.tsx +24 -3
  95. package/src/universal/Checkbox/index.android.tsx +24 -5
  96. package/src/universal/ScrollView/index.ios.tsx +14 -3
  97. package/src/universal/Switch/index.android.tsx +23 -4
  98. package/src/universal/Switch/index.tsx +1 -0
  99. package/src/universal/Text/index.tsx +2 -2
  100. package/src/universal/webUtils.ts +4 -0
  101. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20-sources.jar.md5 +0 -1
  102. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20-sources.jar.sha1 +0 -1
  103. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20-sources.jar.sha256 +0 -1
  104. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20-sources.jar.sha512 +0 -1
  105. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.aar +0 -0
  106. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.aar.md5 +0 -1
  107. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.aar.sha1 +0 -1
  108. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.aar.sha256 +0 -1
  109. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.aar.sha512 +0 -1
  110. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.module.md5 +0 -1
  111. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.module.sha1 +0 -1
  112. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.module.sha256 +0 -1
  113. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.module.sha512 +0 -1
  114. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.pom.md5 +0 -1
  115. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.pom.sha1 +0 -1
  116. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.pom.sha256 +0 -1
  117. package/local-maven-repo/expo/modules/ui/expo.modules.ui/58.0.20/expo.modules.ui-58.0.20.pom.sha512 +0 -1
@@ -47,6 +47,24 @@ struct RNHostView: ExpoSwiftUI.View {
47
47
  isEnabled: !props.layoutRoot
48
48
  )
49
49
  )
50
+ .onAppear {
51
+ updateTouchHandler()
52
+ }
53
+ .onChange(of: props.children?.first?.childIdentity) { _ in
54
+ updateTouchHandler()
55
+ }
56
+ .onDisappear {
57
+ touchHandler.detach()
58
+ }
59
+ }
60
+
61
+ // Follows the current child, which Fabric can replace or remount while this view stays.
62
+ private func updateTouchHandler() {
63
+ if props.layoutRoot, let view = firstChildUIView {
64
+ touchHandler.attach(to: view)
65
+ } else {
66
+ touchHandler.detach()
67
+ }
50
68
  }
51
69
 
52
70
  @ViewBuilder
@@ -60,14 +78,6 @@ struct RNHostView: ExpoSwiftUI.View {
60
78
  ) {
61
79
  Children()
62
80
  }
63
- .onAppear {
64
- if props.layoutRoot {
65
- touchHandler.attach(to: childUIView)
66
- }
67
- }
68
- .onDisappear {
69
- touchHandler.detach()
70
- }
71
81
  } else if matchesContents {
72
82
  // No hosted UIView (a pure SwiftUI child, e.g. Text/Image). Render it at its
73
83
  // natural size instead of falling into the fill branch below, which would
@@ -82,14 +92,6 @@ struct RNHostView: ExpoSwiftUI.View {
82
92
  Children()
83
93
  .frame(maxWidth: .infinity, maxHeight: .infinity)
84
94
  .modifier(ReportSizeToYogaNodeModifier(shadowNodeProxy: props.shadowNodeProxy))
85
- .onAppear {
86
- if props.layoutRoot, let view = firstChildUIView {
87
- touchHandler.attach(to: view)
88
- }
89
- }
90
- .onDisappear {
91
- touchHandler.detach()
92
- }
93
95
  }
94
96
  }
95
97
 
@@ -173,6 +175,7 @@ private final class RNHostTouchHandler: ObservableObject {
173
175
  // An axis that is not matched fills the parent and reports its size to the Yoga node instead
174
176
  private struct ApplySizeFromYogaNode<Content: SwiftUI.View>: SwiftUI.View {
175
177
  @StateObject private var observer: Observer
178
+ let childUIView: UIView
176
179
  let horizontal: Bool
177
180
  let vertical: Bool
178
181
  let shadowNodeProxy: ExpoSwiftUI.ShadowNodeProxy
@@ -186,6 +189,7 @@ private struct ApplySizeFromYogaNode<Content: SwiftUI.View>: SwiftUI.View {
186
189
  @ViewBuilder content: () -> Content
187
190
  ) {
188
191
  _observer = StateObject(wrappedValue: Observer(view: childUIView))
192
+ self.childUIView = childUIView
189
193
  self.horizontal = horizontal
190
194
  self.vertical = vertical
191
195
  self.shadowNodeProxy = shadowNodeProxy
@@ -193,6 +197,14 @@ private struct ApplySizeFromYogaNode<Content: SwiftUI.View>: SwiftUI.View {
193
197
  }
194
198
 
195
199
  var body: some SwiftUI.View {
200
+ sizedContent
201
+ .onChange(of: childUIView) { newView in
202
+ observer.observe(newView)
203
+ }
204
+ }
205
+
206
+ @ViewBuilder
207
+ private var sizedContent: some SwiftUI.View {
196
208
  if horizontal && vertical {
197
209
  content
198
210
  .frame(width: observer.size.width, height: observer.size.height)
@@ -223,6 +235,12 @@ private struct ApplySizeFromYogaNode<Content: SwiftUI.View>: SwiftUI.View {
223
235
 
224
236
  init(view: UIView) {
225
237
  self.size = view.bounds.size
238
+ observe(view)
239
+ }
240
+
241
+ func observe(_ view: UIView) {
242
+ kvoToken?.invalidate()
243
+ size = view.bounds.size
226
244
  kvoToken = view.observe(\.bounds) { [weak self] view, _ in
227
245
  MainActor.assumeIsolated {
228
246
  self?.size = view.bounds.size
@@ -0,0 +1 @@
1
+ 2487d657c68bf866e677ef379bc97b7de37edb41eb173b6d8d938b81bfcb2293
@@ -0,0 +1 @@
1
+ 6c228c1dcbe9ee23cca52ba20eb724d39d18533e6c31ad738ab5738e5dfc64432b7c79363f7ff772f019f8bda955d0d38ccb3f4331d3236e4beddd70e6e426f4
@@ -0,0 +1 @@
1
+ 7acd625f3cd50ad228fac699424fcff3c77942bb
@@ -0,0 +1 @@
1
+ 59cc7013ee6674f030b4f81641fa80fb1829815d6ab5a7903f0e7479e6adb96e
@@ -0,0 +1 @@
1
+ 0a89be2233ee1dd16b1cb671e0694d6d9a428f7f199e3e3efcdd621090c541d435478b39402a6da271373a70cbed690f989e938ebcbbcd35b740e1bf240dfb64
@@ -3,7 +3,7 @@
3
3
  "component": {
4
4
  "group": "expo.modules.ui",
5
5
  "module": "expo.modules.ui",
6
- "version": "58.0.20",
6
+ "version": "58.0.22",
7
7
  "attributes": {
8
8
  "org.gradle.status": "release"
9
9
  }
@@ -24,13 +24,13 @@
24
24
  },
25
25
  "files": [
26
26
  {
27
- "name": "expo.modules.ui-58.0.20.aar",
28
- "url": "expo.modules.ui-58.0.20.aar",
29
- "size": 1859361,
30
- "sha512": "ba561fa50c9fde30184d2c36051e644fd87db59555b7b38f8e2e5b7096b03bf0d2aec3e53fff1831d2705d85b174ec1a7fc2e9cadc69c41237aac262bbea9365",
31
- "sha256": "6cbb10f1e4cd5618c6997d87c2021e97c7bab1b8fc4ecc7be48f563e9c0e284a",
32
- "sha1": "93a6d734857d79049d179014133d651be167840a",
33
- "md5": "b51953e6905c7a42506ea0d23c859703"
27
+ "name": "expo.modules.ui-58.0.22.aar",
28
+ "url": "expo.modules.ui-58.0.22.aar",
29
+ "size": 1882334,
30
+ "sha512": "0a89be2233ee1dd16b1cb671e0694d6d9a428f7f199e3e3efcdd621090c541d435478b39402a6da271373a70cbed690f989e938ebcbbcd35b740e1bf240dfb64",
31
+ "sha256": "59cc7013ee6674f030b4f81641fa80fb1829815d6ab5a7903f0e7479e6adb96e",
32
+ "sha1": "7acd625f3cd50ad228fac699424fcff3c77942bb",
33
+ "md5": "6b7272e2ff6f2f155c76a4e7a3341af6"
34
34
  }
35
35
  ]
36
36
  },
@@ -141,13 +141,13 @@
141
141
  ],
142
142
  "files": [
143
143
  {
144
- "name": "expo.modules.ui-58.0.20.aar",
145
- "url": "expo.modules.ui-58.0.20.aar",
146
- "size": 1859361,
147
- "sha512": "ba561fa50c9fde30184d2c36051e644fd87db59555b7b38f8e2e5b7096b03bf0d2aec3e53fff1831d2705d85b174ec1a7fc2e9cadc69c41237aac262bbea9365",
148
- "sha256": "6cbb10f1e4cd5618c6997d87c2021e97c7bab1b8fc4ecc7be48f563e9c0e284a",
149
- "sha1": "93a6d734857d79049d179014133d651be167840a",
150
- "md5": "b51953e6905c7a42506ea0d23c859703"
144
+ "name": "expo.modules.ui-58.0.22.aar",
145
+ "url": "expo.modules.ui-58.0.22.aar",
146
+ "size": 1882334,
147
+ "sha512": "0a89be2233ee1dd16b1cb671e0694d6d9a428f7f199e3e3efcdd621090c541d435478b39402a6da271373a70cbed690f989e938ebcbbcd35b740e1bf240dfb64",
148
+ "sha256": "59cc7013ee6674f030b4f81641fa80fb1829815d6ab5a7903f0e7479e6adb96e",
149
+ "sha1": "7acd625f3cd50ad228fac699424fcff3c77942bb",
150
+ "md5": "6b7272e2ff6f2f155c76a4e7a3341af6"
151
151
  }
152
152
  ]
153
153
  },
@@ -161,13 +161,13 @@
161
161
  },
162
162
  "files": [
163
163
  {
164
- "name": "expo.modules.ui-58.0.20-sources.jar",
165
- "url": "expo.modules.ui-58.0.20-sources.jar",
166
- "size": 113113,
167
- "sha512": "10ed672c04082cf5f06e6ca6dca0f28229f11f3d503d46e5b61dbc7d3ad50bfdc1399838dd38dc5b8c1643ec375c4bf1672c95606ce28f7ed36384f66b829b68",
168
- "sha256": "f9a24faad34f9f66bebcdde7d05a6cf72a8b4b9030819b80049de5084a694ef0",
169
- "sha1": "5d7c4fd251da7578f23af5102d539533aefb3e64",
170
- "md5": "83ac1d8d00a142a3ffa834c4a6c1db65"
164
+ "name": "expo.modules.ui-58.0.22-sources.jar",
165
+ "url": "expo.modules.ui-58.0.22-sources.jar",
166
+ "size": 115385,
167
+ "sha512": "6c228c1dcbe9ee23cca52ba20eb724d39d18533e6c31ad738ab5738e5dfc64432b7c79363f7ff772f019f8bda955d0d38ccb3f4331d3236e4beddd70e6e426f4",
168
+ "sha256": "2487d657c68bf866e677ef379bc97b7de37edb41eb173b6d8d938b81bfcb2293",
169
+ "sha1": "a1628408f238e018286c0c9887ebb3cf9912577d",
170
+ "md5": "3367880bd9dfec581496b8b5087f9ae0"
171
171
  }
172
172
  ]
173
173
  }
@@ -0,0 +1 @@
1
+ 0ca2cbc2529c5570d31a5cbd827f2b9c58602e25
@@ -0,0 +1 @@
1
+ 113bc88759a3cbbb991dc12571a1b53722855970232edf6508469dc52c822423
@@ -0,0 +1 @@
1
+ ef4e3b199c2d69deab60d857ef81dc8c786f2bdc5a4321e9397111507d9f235c8575cd91289d8c612666b901cadd7aeca414aece1769dca15ec2088f5b8c0cb4
@@ -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.20</version>
12
+ <version>58.0.22</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
+ 2a13240cd35aa73ed4d9787b4a70cbe0453431dd
@@ -0,0 +1 @@
1
+ c15bb9d564da8ea75a501e89d3c855df5294966438d81168675388dab731e23e
@@ -0,0 +1 @@
1
+ a1a75142fcd486afa2e7bd2f27cdc21e1997cb309fb762963b61e65c88fd6e5a62af5234f448b485b0a2f3081f74eb1c8664e9d08a1f1a445ca2c62639e7f3f1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/ui",
3
- "version": "58.0.20",
3
+ "version": "58.0.22",
4
4
  "description": "A collection of UI components",
5
5
  "keywords": [
6
6
  "Jetpack Compose",
@@ -141,7 +141,7 @@
141
141
  },
142
142
  "devDependencies": {
143
143
  "@babel/core": "^7.26.0",
144
- "@expo/internal-scripts": "58.0.3",
144
+ "@expo/internal-scripts": "58.0.4",
145
145
  "@playwright/test": "^1.59.0",
146
146
  "@testing-library/react": "^16.3.0",
147
147
  "@testing-library/react-native": "^14.0.1",
@@ -135,10 +135,14 @@ export function BottomSheet(props: BottomSheetProps) {
135
135
  } else if (indexProp >= 0) {
136
136
  closedRef.current = false;
137
137
  setIsPresented(true);
138
- const clampedIndex = Math.min(indexProp, detents.length - 1);
139
- setCurrentIndex(clampedIndex);
140
- currentIndexRef.current = clampedIndex;
141
138
  }
139
+ }, [indexProp]);
140
+
141
+ useEffect(() => {
142
+ const requestedIndex = indexProp >= 0 ? indexProp : currentIndexRef.current;
143
+ const clampedIndex = Math.min(requestedIndex, detents.length - 1);
144
+ setCurrentIndex(clampedIndex);
145
+ currentIndexRef.current = clampedIndex;
142
146
  }, [indexProp, detents.length]);
143
147
 
144
148
  const handlePresentedChange = useCallback((presented: boolean) => {
@@ -9,7 +9,7 @@ import {
9
9
  useState,
10
10
  type ReactElement,
11
11
  } from 'react';
12
- import { Platform } from 'react-native';
12
+ import { Platform, StyleSheet, View } from 'react-native';
13
13
 
14
14
  import { useNativeState, worklets } from '../../State';
15
15
  import { Group } from '../../swift-ui/Group';
@@ -89,6 +89,12 @@ export function PagerView(props: PagerViewProps) {
89
89
  const pageCountRef = useRef(0);
90
90
  pageCountRef.current = pageCount;
91
91
 
92
+ const isScrollingRef = useRef(false);
93
+ // The SwiftUI scroll doesn't cancel React Native touches on its pages. Like
94
+ // React Native's `ScrollView`, take the touch from the page content while
95
+ // the pager moves, so a swipe or a tap that stops it doesn't press anything.
96
+ const shouldCaptureTouch = () => isScrollingRef.current;
97
+
92
98
  // Clamp on first render — out-of-range initials produce an id that matches
93
99
  // no `Group`, leaving `.scrollPosition(id:)` silently stuck at 0.
94
100
  const [clampedInitialPage] = useState(() => {
@@ -188,17 +194,24 @@ export function PagerView(props: PagerViewProps) {
188
194
  );
189
195
  const geometryModifier = useScrollGeometryChange(handleScrollGeometry);
190
196
 
191
- const phaseModifier = onPageScrollStateChanged
192
- ? onScrollPhaseChange((phase) =>
193
- onPageScrollStateChanged(wrapNativeEvent({ pageScrollState: phaseToPageState(phase) }))
194
- )
195
- : null;
197
+ const phaseModifier = onScrollPhaseChange((phase) => {
198
+ const pageScrollState = phaseToPageState(phase);
199
+ isScrollingRef.current = pageScrollState !== 'idle';
200
+ onPageScrollStateChanged?.(wrapNativeEvent({ pageScrollState }));
201
+ });
196
202
 
197
203
  const pages = validChildren.map((child, index) => (
198
204
  <Group
199
205
  key={child.key ?? String(index)}
200
206
  modifiers={[containerRelativeFrame({ axes: 'horizontal' }), id(String(index))]}>
201
- <RNHostView>{child}</RNHostView>
207
+ <RNHostView>
208
+ <View
209
+ style={styles.page}
210
+ onStartShouldSetResponderCapture={shouldCaptureTouch}
211
+ onMoveShouldSetResponderCapture={shouldCaptureTouch}>
212
+ {child}
213
+ </View>
214
+ </RNHostView>
202
215
  </Group>
203
216
  ));
204
217
 
@@ -209,7 +222,7 @@ export function PagerView(props: PagerViewProps) {
209
222
  scrollDisabled(!scrollEnabledState),
210
223
  scrollPosition(activePageState, { onChange: handleScrolledIDChange }),
211
224
  ...(geometryModifier ? [geometryModifier] : []),
212
- ...(phaseModifier ? [phaseModifier] : []),
225
+ phaseModifier,
213
226
  ];
214
227
 
215
228
  return (
@@ -271,3 +284,7 @@ function warnIfPreIOS18ScrollCallbacksDropped(
271
284
  );
272
285
  }
273
286
  }
287
+
288
+ const styles = StyleSheet.create({
289
+ page: { flex: 1 },
290
+ });
@@ -1,7 +1,11 @@
1
1
  import { Host } from '../../swift-ui/Host';
2
2
  import { Picker } from '../../swift-ui/Picker';
3
3
  import { Text } from '../../swift-ui/Text';
4
- import { disabled as disabledModifier } from '../../swift-ui/modifiers';
4
+ import {
5
+ accessibilityLabel as accessibilityLabelModifier,
6
+ disabled as disabledModifier,
7
+ } from '../../swift-ui/modifiers';
8
+ import { createModifier } from '../../swift-ui/modifiers/createModifier';
5
9
  import { environment } from '../../swift-ui/modifiers/environment';
6
10
  import { pickerStyle } from '../../swift-ui/modifiers/pickerStyle';
7
11
  import { tag } from '../../swift-ui/modifiers/tag';
@@ -15,15 +19,23 @@ export function SegmentedControl(props: SegmentedControlProps) {
15
19
  enabled = true,
16
20
  onChange,
17
21
  onValueChange,
22
+ tintColor,
18
23
  appearance,
19
24
  style,
20
25
  testID,
26
+ accessibilityLabel,
21
27
  } = props;
22
28
 
23
29
  const modifiers: ModifierConfig[] = [pickerStyle('segmented')];
24
30
  if (!enabled) {
25
31
  modifiers.push(disabledModifier());
26
32
  }
33
+ if (accessibilityLabel) {
34
+ modifiers.push(accessibilityLabelModifier(accessibilityLabel));
35
+ }
36
+ if (tintColor) {
37
+ modifiers.push(createModifier('selectedSegmentTintColor', { color: tintColor }));
38
+ }
27
39
  if (appearance) {
28
40
  modifiers.push(environment('colorScheme', appearance));
29
41
  }
@@ -43,7 +43,6 @@ export interface SegmentedControlProps {
43
43
  onValueChange?: (value: string) => void;
44
44
  /**
45
45
  * Accent color of the control.
46
- * @platform android, web
47
46
  */
48
47
  tintColor?: string;
49
48
  /**
@@ -52,6 +51,11 @@ export interface SegmentedControlProps {
52
51
  appearance?: 'dark' | 'light';
53
52
  style?: StyleProp<ViewStyle>;
54
53
  testID?: string;
54
+ /**
55
+ * The name of the control that screen readers announce.
56
+ * @platform ios
57
+ */
58
+ accessibilityLabel?: string;
55
59
  }
56
60
 
57
61
  /**
@@ -5,7 +5,15 @@
5
5
  */
6
6
 
7
7
  import * as React from 'react';
8
- import { Animated, Easing, I18nManager, StyleSheet, View, useColorScheme } from 'react-native';
8
+ import {
9
+ Animated,
10
+ Easing,
11
+ I18nManager,
12
+ Platform,
13
+ StyleSheet,
14
+ View,
15
+ useColorScheme,
16
+ } from 'react-native';
9
17
 
10
18
  import { SegmentedControlTab } from './SegmentedControlTab';
11
19
  import { SegmentsSeparators } from './SegmentsSeparators';
@@ -53,7 +61,7 @@ export function SegmentedControl({
53
61
  toValue: isRTL * (selectedIndex ?? 0),
54
62
  duration: 300,
55
63
  easing: Easing.out(Easing.quad),
56
- useNativeDriver: true,
64
+ useNativeDriver: Platform.OS !== 'web',
57
65
  }).start();
58
66
  }
59
67
  }, [animation, segmentWidth, selectedIndex]);
@@ -0,0 +1,43 @@
1
+ import { requireNativeView } from 'expo';
2
+ import type { ReactNode } from 'react';
3
+
4
+ /**
5
+ * Composition local values that `CompositionLocalProvider` can provide.
6
+ */
7
+ export type CompositionLocalValues = {
8
+ /**
9
+ * The minimum size, in dp, that Material components such as `Button`, `IconButton`, `Checkbox`, and
10
+ * `Switch` reserve for their touch target. Maps to `LocalMinimumInteractiveComponentSize`.
11
+ * Set it to `0` to let these components be smaller than Material's default of 48dp.
12
+ */
13
+ minimumInteractiveComponentSize?: number;
14
+ };
15
+
16
+ export type CompositionLocalProviderProps = {
17
+ /**
18
+ * The values to provide to the children.
19
+ */
20
+ values: CompositionLocalValues;
21
+ /**
22
+ * The content that receives the values.
23
+ */
24
+ children?: ReactNode;
25
+ };
26
+
27
+ type NativeCompositionLocalProviderProps = CompositionLocalValues & {
28
+ children?: ReactNode;
29
+ };
30
+
31
+ const CompositionLocalProviderNativeView: React.ComponentType<NativeCompositionLocalProviderProps> =
32
+ requireNativeView('ExpoUI', 'CompositionLocalProviderView');
33
+
34
+ /**
35
+ * Provides composition local values to its children, like Jetpack Compose's `CompositionLocalProvider`.
36
+ * It adds no layout of its own, so children keep modifiers such as `weight` from the parent `Row`
37
+ * or `Column`.
38
+ */
39
+ export function CompositionLocalProvider({ values, children }: CompositionLocalProviderProps) {
40
+ return (
41
+ <CompositionLocalProviderNativeView {...values}>{children}</CompositionLocalProviderNativeView>
42
+ );
43
+ }
@@ -6,13 +6,19 @@ import {
6
6
  type StyleProp,
7
7
  type ViewStyle,
8
8
  I18nManager,
9
+ processColor,
9
10
  useColorScheme as useRNColorScheme,
10
11
  } from 'react-native';
11
12
 
12
13
  import { NativeTreeContext, useWarnIfNestedHost } from '../../NativeTreeContext';
13
14
  import { TextInputHostProvider, useTextInputHostRef } from '../../keyboard';
14
15
  import { useMergeRefs } from '../../utils/useMergeRefs';
15
- import { getMaterialColors, HostPaletteContext } from '../colors';
16
+ import {
17
+ getMaterialColors,
18
+ HostPaletteContext,
19
+ type MaterialColors,
20
+ type RgbaHex,
21
+ } from '../colors';
16
22
  import { type PrimitiveBaseProps } from '../layout';
17
23
 
18
24
  export interface HostProps extends PrimitiveBaseProps {
@@ -52,6 +58,14 @@ export interface HostProps extends PrimitiveBaseProps {
52
58
  */
53
59
  seedColor?: ColorValue;
54
60
 
61
+ /**
62
+ * Material 3 color roles that replace the matching roles of the palette from
63
+ * `colorScheme` and `seedColor`. Roles you omit keep their palette value.
64
+ * The overrides theme Compose children and are returned by `useMaterialColors()`
65
+ * in the same `#RRGGBBAA` form as the other roles.
66
+ */
67
+ colorOverrides?: Partial<MaterialColors>;
68
+
55
69
  /**
56
70
  * The layout direction for the content.
57
71
  * Defaults to the current locale direction from I18nManager.
@@ -93,6 +107,7 @@ export function Host(props: HostProps) {
93
107
  layoutDirection,
94
108
  colorScheme,
95
109
  seedColor,
110
+ colorOverrides,
96
111
  ref,
97
112
  children,
98
113
  ...restProps
@@ -101,10 +116,19 @@ export function Host(props: HostProps) {
101
116
 
102
117
  const systemScheme = useRNColorScheme();
103
118
  const resolvedScheme = schemeString ?? (systemScheme === 'dark' ? 'dark' : 'light');
104
- const palette = useMemo(
105
- () => getMaterialColors({ scheme: resolvedScheme, seedColor }),
106
- [resolvedScheme, seedColor]
119
+ // Key on the normalized values, not the object, so an inline `colorOverrides` object keeps
120
+ // the palette from `useMaterialColors()` reference-stable across re-renders.
121
+ const overridesKey = colorOverrides
122
+ ? JSON.stringify(normalizeColorOverrides(colorOverrides))
123
+ : null;
124
+ const normalizedOverrides = useMemo<Partial<MaterialColors> | undefined>(
125
+ () => (overridesKey ? JSON.parse(overridesKey) : undefined),
126
+ [overridesKey]
107
127
  );
128
+ const palette = useMemo(() => {
129
+ const basePalette = getMaterialColors({ scheme: resolvedScheme, seedColor });
130
+ return normalizedOverrides ? { ...basePalette, ...normalizedOverrides } : basePalette;
131
+ }, [resolvedScheme, seedColor, normalizedOverrides]);
108
132
  const hostRef = useTextInputHostRef();
109
133
  const mergedRef = useMergeRefs(ref, hostRef);
110
134
  useWarnIfNestedHost('Jetpack Compose');
@@ -123,6 +147,7 @@ export function Host(props: HostProps) {
123
147
  }
124
148
  colorScheme={schemeString}
125
149
  seedColor={seedColor}
150
+ colorOverrides={normalizedOverrides}
126
151
  onLayoutContent={onLayoutContent}
127
152
  layoutDirection={
128
153
  layoutDirection ?? (I18nManager.getConstants().isRTL ? 'rightToLeft' : 'leftToRight')
@@ -134,3 +159,24 @@ export function Host(props: HostProps) {
134
159
  </HostPaletteContext.Provider>
135
160
  );
136
161
  }
162
+
163
+ function normalizeColorOverrides(overrides: Partial<MaterialColors>): Partial<MaterialColors> {
164
+ const normalized: Partial<MaterialColors> = {};
165
+ for (const [role, color] of Object.entries(overrides) as [keyof MaterialColors, RgbaHex][]) {
166
+ const hex = color != null ? toRgbaHex(color) : null;
167
+ if (hex) {
168
+ normalized[role] = hex;
169
+ }
170
+ }
171
+ return normalized;
172
+ }
173
+
174
+ // `processColor` returns `0xAARRGGBB`; the palette uses `#RRGGBBAA` like native `toTokenMap()`.
175
+ function toRgbaHex(color: RgbaHex): RgbaHex | null {
176
+ const argb = processColor(color);
177
+ if (typeof argb !== 'number') {
178
+ return null;
179
+ }
180
+ const rgba = ((argb << 8) | (argb >>> 24)) >>> 0;
181
+ return `#${rgba.toString(16).padStart(8, '0').toUpperCase()}`;
182
+ }
@@ -39,6 +39,8 @@ export interface SwitchProps {
39
39
  enabled?: boolean;
40
40
  /**
41
41
  * Callback function that is called when the checked state changes.
42
+ * When omitted, the switch is not interactive on its own, so a parent with the
43
+ * `toggleable` modifier can handle input and accessibility for it.
42
44
  */
43
45
  onCheckedChange?: (value: boolean) => void;
44
46
  /**
@@ -58,6 +60,7 @@ export interface SwitchProps {
58
60
 
59
61
  type NativeSwitchProps = Omit<SwitchProps, 'onCheckedChange' | 'children'> & {
60
62
  children?: React.ReactNode;
63
+ nativeClickable: boolean;
61
64
  onCheckedChange: (event: NativeSyntheticEvent<{ value: boolean }>) => void;
62
65
  };
63
66
 
@@ -94,6 +97,7 @@ function transformSwitchProps(props: SwitchProps): Omit<NativeSwitchProps, 'chil
94
97
  modifiers,
95
98
  ...(modifiers ? createViewModifierEventListener(modifiers) : undefined),
96
99
  ...restProps,
100
+ nativeClickable: onCheckedChange != null,
97
101
  onCheckedChange: ({ nativeEvent: { value } }) => {
98
102
  onCheckedChange?.(value);
99
103
  },