@expo/ui 56.0.8 → 56.0.10

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 (252) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/CLAUDE.md +1 -1
  3. package/android/build.gradle +2 -2
  4. package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +49 -6
  5. package/android/src/main/java/expo/modules/ui/HorizontalPagerView.kt +97 -16
  6. package/android/src/main/java/expo/modules/ui/LoadingView.kt +80 -0
  7. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +31 -3
  8. package/android/src/main/java/expo/modules/ui/SnackbarView.kt +126 -0
  9. package/android/src/main/java/expo/modules/ui/colors/MaterialColors.kt +2 -0
  10. package/android/src/main/java/expo/modules/ui/state/ObservableState.kt +10 -0
  11. package/assets/keyboard_arrow_down.xml +10 -0
  12. package/build/State/index.d.ts +6 -0
  13. package/build/State/index.d.ts.map +1 -0
  14. package/build/State/useNativeState.d.ts +32 -3
  15. package/build/State/useNativeState.d.ts.map +1 -1
  16. package/build/community/bottom-sheet/BottomSheet.ios.d.ts.map +1 -1
  17. package/build/community/pager-view/PagerView.android.d.ts +7 -0
  18. package/build/community/pager-view/PagerView.android.d.ts.map +1 -0
  19. package/build/community/pager-view/PagerView.d.ts +8 -0
  20. package/build/community/pager-view/PagerView.d.ts.map +1 -0
  21. package/build/community/pager-view/PagerView.ios.d.ts +15 -0
  22. package/build/community/pager-view/PagerView.ios.d.ts.map +1 -0
  23. package/build/community/pager-view/index.d.ts +3 -0
  24. package/build/community/pager-view/index.d.ts.map +1 -0
  25. package/build/community/pager-view/types.d.ts +128 -0
  26. package/build/community/pager-view/types.d.ts.map +1 -0
  27. package/build/community/segmented-control/vendor/SegmentsSeparators.d.ts.map +1 -1
  28. package/build/jetpack-compose/HorizontalPager/index.d.ts +27 -0
  29. package/build/jetpack-compose/HorizontalPager/index.d.ts.map +1 -1
  30. package/build/jetpack-compose/Host/index.d.ts +1 -0
  31. package/build/jetpack-compose/Host/index.d.ts.map +1 -1
  32. package/build/jetpack-compose/LoadingIndicator/index.d.ts +41 -0
  33. package/build/jetpack-compose/LoadingIndicator/index.d.ts.map +1 -0
  34. package/build/jetpack-compose/Snackbar/index.d.ts +94 -0
  35. package/build/jetpack-compose/Snackbar/index.d.ts.map +1 -0
  36. package/build/jetpack-compose/SyncSwitch/index.d.ts +1 -1
  37. package/build/jetpack-compose/SyncSwitch/index.d.ts.map +1 -1
  38. package/build/jetpack-compose/TextField/index.d.ts +1 -1
  39. package/build/jetpack-compose/TextField/index.d.ts.map +1 -1
  40. package/build/jetpack-compose/index.d.ts +3 -2
  41. package/build/jetpack-compose/index.d.ts.map +1 -1
  42. package/build/jetpack-compose/modifiers/index.d.ts +6 -2
  43. package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
  44. package/build/swift-ui/BottomSheet/index.d.ts +5 -1
  45. package/build/swift-ui/BottomSheet/index.d.ts.map +1 -1
  46. package/build/swift-ui/Host/index.d.ts +1 -0
  47. package/build/swift-ui/Host/index.d.ts.map +1 -1
  48. package/build/swift-ui/ScrollView/index.d.ts +30 -0
  49. package/build/swift-ui/ScrollView/index.d.ts.map +1 -1
  50. package/build/swift-ui/SecureField/index.d.ts +1 -1
  51. package/build/swift-ui/SecureField/index.d.ts.map +1 -1
  52. package/build/swift-ui/SyncToggle/index.d.ts +1 -1
  53. package/build/swift-ui/SyncToggle/index.d.ts.map +1 -1
  54. package/build/swift-ui/TextField/index.d.ts +1 -1
  55. package/build/swift-ui/TextField/index.d.ts.map +1 -1
  56. package/build/swift-ui/index.d.ts +2 -2
  57. package/build/swift-ui/index.d.ts.map +1 -1
  58. package/build/swift-ui/modifiers/index.d.ts +25 -15
  59. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  60. package/build/swift-ui/modifiers/scrollObservation.d.ts +52 -0
  61. package/build/swift-ui/modifiers/scrollObservation.d.ts.map +1 -0
  62. package/build/swift-ui/modifiers/scrollPosition.d.ts +1 -1
  63. package/build/swift-ui/modifiers/scrollPosition.d.ts.map +1 -1
  64. package/build/swift-ui/modifiers/symbolEffect.d.ts +1 -1
  65. package/build/swift-ui/modifiers/symbolEffect.d.ts.map +1 -1
  66. package/build/swift-ui/withAnimation.d.ts +26 -0
  67. package/build/swift-ui/withAnimation.d.ts.map +1 -0
  68. package/build/universal/BottomSheet/index.android.d.ts +1 -1
  69. package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
  70. package/build/universal/BottomSheet/index.d.ts +1 -1
  71. package/build/universal/BottomSheet/index.d.ts.map +1 -1
  72. package/build/universal/BottomSheet/index.ios.d.ts +1 -1
  73. package/build/universal/BottomSheet/index.ios.d.ts.map +1 -1
  74. package/build/universal/BottomSheet/types.d.ts +27 -0
  75. package/build/universal/BottomSheet/types.d.ts.map +1 -1
  76. package/build/universal/Checkbox/index.d.ts.map +1 -1
  77. package/build/universal/Collapsible/index.android.d.ts +8 -0
  78. package/build/universal/Collapsible/index.android.d.ts.map +1 -0
  79. package/build/universal/Collapsible/index.d.ts +8 -0
  80. package/build/universal/Collapsible/index.d.ts.map +1 -0
  81. package/build/universal/Collapsible/index.ios.d.ts +7 -0
  82. package/build/universal/Collapsible/index.ios.d.ts.map +1 -0
  83. package/build/universal/Collapsible/types.d.ts +23 -0
  84. package/build/universal/Collapsible/types.d.ts.map +1 -0
  85. package/build/universal/Column/index.d.ts.map +1 -1
  86. package/build/universal/Host/index.d.ts +5 -18
  87. package/build/universal/Host/index.d.ts.map +1 -1
  88. package/build/universal/Host/types.d.ts +72 -0
  89. package/build/universal/Host/types.d.ts.map +1 -0
  90. package/build/universal/List/index.android.d.ts +9 -0
  91. package/build/universal/List/index.android.d.ts.map +1 -0
  92. package/build/universal/List/index.d.ts +8 -0
  93. package/build/universal/List/index.d.ts.map +1 -0
  94. package/build/universal/List/index.ios.d.ts +8 -0
  95. package/build/universal/List/index.ios.d.ts.map +1 -0
  96. package/build/universal/List/types.d.ts +26 -0
  97. package/build/universal/List/types.d.ts.map +1 -0
  98. package/build/universal/ListItem/ListItem.android.d.ts +8 -0
  99. package/build/universal/ListItem/ListItem.android.d.ts.map +1 -0
  100. package/build/universal/ListItem/ListItem.d.ts +9 -0
  101. package/build/universal/ListItem/ListItem.d.ts.map +1 -0
  102. package/build/universal/ListItem/ListItem.ios.d.ts +8 -0
  103. package/build/universal/ListItem/ListItem.ios.d.ts.map +1 -0
  104. package/build/universal/ListItem/ListItemSlots.d.ts +21 -0
  105. package/build/universal/ListItem/ListItemSlots.d.ts.map +1 -0
  106. package/build/universal/ListItem/index.d.ts +10 -0
  107. package/build/universal/ListItem/index.d.ts.map +1 -0
  108. package/build/universal/ListItem/types.d.ts +59 -0
  109. package/build/universal/ListItem/types.d.ts.map +1 -0
  110. package/build/universal/Picker/Picker.android.d.ts +9 -0
  111. package/build/universal/Picker/Picker.android.d.ts.map +1 -0
  112. package/build/universal/Picker/Picker.d.ts +8 -0
  113. package/build/universal/Picker/Picker.d.ts.map +1 -0
  114. package/build/universal/Picker/Picker.ios.d.ts +9 -0
  115. package/build/universal/Picker/Picker.ios.d.ts.map +1 -0
  116. package/build/universal/Picker/PickerItem.d.ts +9 -0
  117. package/build/universal/Picker/PickerItem.d.ts.map +1 -0
  118. package/build/universal/Picker/index.d.ts +8 -0
  119. package/build/universal/Picker/index.d.ts.map +1 -0
  120. package/build/universal/Picker/types.d.ts +69 -0
  121. package/build/universal/Picker/types.d.ts.map +1 -0
  122. package/build/universal/RNHostView/index.android.d.ts +7 -0
  123. package/build/universal/RNHostView/index.android.d.ts.map +1 -0
  124. package/build/universal/RNHostView/index.d.ts +7 -0
  125. package/build/universal/RNHostView/index.d.ts.map +1 -0
  126. package/build/universal/RNHostView/index.ios.d.ts +7 -0
  127. package/build/universal/RNHostView/index.ios.d.ts.map +1 -0
  128. package/build/universal/RNHostView/types.d.ts +23 -0
  129. package/build/universal/RNHostView/types.d.ts.map +1 -0
  130. package/build/universal/Switch/index.d.ts.map +1 -1
  131. package/build/universal/TextInput/index.d.ts.map +1 -1
  132. package/build/universal/index.d.ts +5 -0
  133. package/build/universal/index.d.ts.map +1 -1
  134. package/expo-module.config.json +1 -1
  135. package/ios/BottomSheetView.swift +4 -1
  136. package/ios/ExpoUIModule.swift +47 -4
  137. package/ios/HostView.swift +21 -18
  138. package/ios/Modifiers/AnimationConfig.swift +109 -0
  139. package/ios/Modifiers/FontModifier.swift +72 -20
  140. package/ios/Modifiers/ScrollObservationModifiers.swift +107 -0
  141. package/ios/Modifiers/ViewModifierRegistry.swift +9 -112
  142. package/ios/State/ObservableState.swift +12 -1
  143. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.8/expo.modules.ui-56.0.8-sources.jar → 56.0.10/expo.modules.ui-56.0.10-sources.jar} +0 -0
  144. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10-sources.jar.md5 +1 -0
  145. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10-sources.jar.sha1 +1 -0
  146. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10-sources.jar.sha256 +1 -0
  147. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10-sources.jar.sha512 +1 -0
  148. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.aar +0 -0
  149. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.aar.md5 +1 -0
  150. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.aar.sha1 +1 -0
  151. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.aar.sha256 +1 -0
  152. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.aar.sha512 +1 -0
  153. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.8/expo.modules.ui-56.0.8.module → 56.0.10/expo.modules.ui-56.0.10.module} +22 -22
  154. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.module.md5 +1 -0
  155. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.module.sha1 +1 -0
  156. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.module.sha256 +1 -0
  157. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.module.sha512 +1 -0
  158. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.8/expo.modules.ui-56.0.8.pom → 56.0.10/expo.modules.ui-56.0.10.pom} +1 -1
  159. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.pom.md5 +1 -0
  160. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.pom.sha1 +1 -0
  161. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.pom.sha256 +1 -0
  162. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.10/expo.modules.ui-56.0.10.pom.sha512 +1 -0
  163. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  164. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  165. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  166. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  167. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  168. package/package.json +7 -3
  169. package/src/State/index.ts +10 -0
  170. package/src/State/useNativeState.ts +71 -3
  171. package/src/community/bottom-sheet/BottomSheet.ios.tsx +0 -17
  172. package/src/community/pager-view/PagerView.android.tsx +223 -0
  173. package/src/community/pager-view/PagerView.ios.tsx +267 -0
  174. package/src/community/pager-view/PagerView.tsx +14 -0
  175. package/src/community/pager-view/index.tsx +13 -0
  176. package/src/community/pager-view/types.tsx +137 -0
  177. package/src/community/picker/Picker.android.tsx +1 -1
  178. package/src/community/segmented-control/vendor/SegmentsSeparators.tsx +3 -6
  179. package/src/jetpack-compose/HorizontalPager/index.tsx +89 -18
  180. package/src/jetpack-compose/Host/index.tsx +1 -0
  181. package/src/jetpack-compose/LoadingIndicator/index.tsx +91 -0
  182. package/src/jetpack-compose/Snackbar/index.tsx +135 -0
  183. package/src/jetpack-compose/SyncSwitch/index.tsx +1 -3
  184. package/src/jetpack-compose/TextField/index.tsx +1 -4
  185. package/src/jetpack-compose/index.ts +3 -2
  186. package/src/jetpack-compose/modifiers/index.ts +5 -2
  187. package/src/swift-ui/BottomSheet/index.tsx +32 -15
  188. package/src/swift-ui/Host/index.tsx +1 -0
  189. package/src/swift-ui/ScrollView/index.tsx +33 -0
  190. package/src/swift-ui/SecureField/index.tsx +1 -4
  191. package/src/swift-ui/SyncToggle/index.tsx +1 -3
  192. package/src/swift-ui/TextField/index.tsx +1 -4
  193. package/src/swift-ui/index.tsx +2 -3
  194. package/src/swift-ui/modifiers/index.ts +37 -14
  195. package/src/swift-ui/modifiers/scrollObservation.ts +80 -0
  196. package/src/swift-ui/modifiers/scrollPosition.ts +1 -2
  197. package/src/swift-ui/modifiers/symbolEffect.ts +1 -2
  198. package/src/swift-ui/withAnimation.ts +71 -0
  199. package/src/ts-declarations/react-native-web.d.ts +7 -0
  200. package/src/universal/BottomSheet/index.android.tsx +58 -11
  201. package/src/universal/BottomSheet/index.ios.tsx +40 -20
  202. package/src/universal/BottomSheet/index.tsx +49 -4
  203. package/src/universal/BottomSheet/types.ts +25 -0
  204. package/src/universal/Checkbox/index.tsx +14 -2
  205. package/src/universal/Collapsible/index.android.tsx +72 -0
  206. package/src/universal/Collapsible/index.ios.tsx +16 -0
  207. package/src/universal/Collapsible/index.tsx +71 -0
  208. package/src/universal/Collapsible/types.ts +25 -0
  209. package/src/universal/Column/index.tsx +3 -1
  210. package/src/universal/Host/index.tsx +9 -10
  211. package/src/universal/Host/types.ts +70 -0
  212. package/src/universal/List/index.android.tsx +44 -0
  213. package/src/universal/List/index.ios.tsx +19 -0
  214. package/src/universal/List/index.tsx +26 -0
  215. package/src/universal/List/types.ts +28 -0
  216. package/src/universal/ListItem/ListItem.android.tsx +52 -0
  217. package/src/universal/ListItem/ListItem.ios.tsx +58 -0
  218. package/src/universal/ListItem/ListItem.tsx +77 -0
  219. package/src/universal/ListItem/ListItemSlots.tsx +66 -0
  220. package/src/universal/ListItem/index.ts +15 -0
  221. package/src/universal/ListItem/types.ts +67 -0
  222. package/src/universal/Picker/Picker.android.tsx +69 -0
  223. package/src/universal/Picker/Picker.ios.tsx +45 -0
  224. package/src/universal/Picker/Picker.tsx +52 -0
  225. package/src/universal/Picker/PickerItem.tsx +27 -0
  226. package/src/universal/Picker/index.ts +11 -0
  227. package/src/universal/Picker/types.ts +79 -0
  228. package/src/universal/RNHostView/index.android.tsx +33 -0
  229. package/src/universal/RNHostView/index.ios.tsx +12 -0
  230. package/src/universal/RNHostView/index.tsx +39 -0
  231. package/src/universal/RNHostView/types.ts +25 -0
  232. package/src/universal/Switch/index.tsx +7 -2
  233. package/src/universal/TextInput/index.tsx +12 -2
  234. package/src/universal/index.ts +5 -0
  235. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.md5 +0 -1
  236. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha1 +0 -1
  237. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha256 +0 -1
  238. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8-sources.jar.sha512 +0 -1
  239. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar +0 -0
  240. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.md5 +0 -1
  241. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha1 +0 -1
  242. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha256 +0 -1
  243. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.aar.sha512 +0 -1
  244. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.md5 +0 -1
  245. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha1 +0 -1
  246. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha256 +0 -1
  247. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.module.sha512 +0 -1
  248. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.md5 +0 -1
  249. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha1 +0 -1
  250. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha256 +0 -1
  251. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.8/expo.modules.ui-56.0.8.pom.sha512 +0 -1
  252. package/src/community/bottom-sheet/CLAUDE.md +0 -55
package/CHANGELOG.md CHANGED
@@ -10,6 +10,37 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.10 — 2026-05-20
14
+
15
+ ### 🎉 New features
16
+
17
+ - Added `@expo/ui/community/pager-view` — a drop-in replacement for `react-native-pager-view`. ([#45499](https://github.com/expo/expo/pull/45499) by [@vonovak](https://github.com/vonovak))
18
+ - [iOS] Added `textStyle` option to `font` modifier in `@expo/ui/swift-ui` for iOS Dynamic Type scaling. ([#46007](https://github.com/expo/expo/pull/46007) by [@ramonclaudio](https://github.com/ramonclaudio))
19
+
20
+ ### 🐛 Bug fixes
21
+
22
+ - [universal] Fix universal components dark theme ([#45969](https://github.com/expo/expo/pull/45969) by [@zoontek](https://github.com/zoontek))
23
+ - [universal] Fix `BottomSheet` behavior by making `Host` optional, and fix Android exit animation. ([#46031](https://github.com/expo/expo/pull/46031) by [@nishan](https://github.com/intergalacticspacehighway))
24
+
25
+ ## 56.0.9 — 2026-05-19
26
+
27
+ ### 🎉 New features
28
+
29
+ - [iOS][android] Added `onChange` listener to `useNativeState`. ([#45961](https://github.com/expo/expo/pull/45961) by [@nishan](https://github.com/intergalacticspacehighway))
30
+ - Allow writing to native state from the JS thread. ([#45901](https://github.com/expo/expo/pull/45901) by [@nishan](https://github.com/intergalacticspacehighway))
31
+ - [iOS] Added `withAnimation(animation, body)` in `@expo/ui/swift-ui`, mirroring SwiftUI's [`withAnimation(_:_:)`](<https://developer.apple.com/documentation/swiftui/withanimation(_:_:)>). ([#45893](https://github.com/expo/expo/pull/45893) by [@nishan](https://github.com/intergalacticspacehighway))
32
+ - [jetpack-compose] Added `Snackbar` component. ([#45667](https://github.com/expo/expo/pull/45667) by [@nishan](https://github.com/intergalacticspacehighway))
33
+ - [android] Added `LoadingIndicator` and `ContainedLoadingIndicator` components. ([#41169](https://github.com/expo/expo/pull/41169) by [@suveshmoza](https://github.com/suveshmoza))
34
+
35
+ ### 🐛 Bug fixes
36
+
37
+ - [iOS] Unmount `BottomSheet` when it is dismissed. ([#45846](https://github.com/expo/expo/pull/45846) by [@nishan](https://github.com/intergalacticspacehighway))
38
+ - [iOS] Apply `modifiers` prop on `Host` instead of silently dropping it. ([#45872](https://github.com/expo/expo/pull/45872) by [@ramonclaudio](https://github.com/ramonclaudio))
39
+
40
+ ### 💡 Others
41
+
42
+ - [universal] Add base styling to universal Picker on web ([#45932](https://github.com/expo/expo/pull/45932) by [@zoontek](https://github.com/zoontek))
43
+
13
44
  ## 56.0.8 — 2026-05-15
14
45
 
15
46
  ### 🎉 New features
@@ -34,6 +65,9 @@ _This version does not introduce any user-facing changes._
34
65
  - Make `ChartView` public. ([#45674](https://github.com/expo/expo/pull/45674) by [@jakex7](https://github.com/jakex7))
35
66
  - Added `@expo/ui/community/menu`, a drop-in replacement for `@react-native-menu/menu`. ([#45670](https://github.com/expo/expo/pull/45670) by [@vonovak](https://github.com/vonovak))
36
67
  - [android] Added Compose `combinedClickable` modifier. ([#45670](https://github.com/expo/expo/pull/45670) by [@vonovak](https://github.com/vonovak))
68
+ - [universal] Added `Collapsible`, `List`, `ListItem`, and `Picker` components. ([#45754](https://github.com/expo/expo/pull/45754) by [@kudo](https://github.com/kudo))
69
+ - [universal] Added `snapPoints` prop on `BottomSheet` and `colorScheme` / `layoutDirection` props on `Host`. ([#45754](https://github.com/expo/expo/pull/45754) by [@kudo](https://github.com/kudo))
70
+ - [jetpack-compose] `background(color, { animationSpec })` accepts an optional `animationSpec` and wraps the color in `animateColorAsState` so changes between renders animate smoothly. ([#45754](https://github.com/expo/expo/pull/45754) by [@kudo](https://github.com/kudo))
37
71
 
38
72
  ### 🐛 Bug fixes
39
73
 
@@ -333,7 +367,7 @@ _This version does not introduce any user-facing changes._
333
367
 
334
368
  - [iOS] Remove leftover `Switch` TypeScript exports from swift-ui package. Use `Toggle` instead. ([#42571](https://github.com/expo/expo/pull/42571) by [@shubh73](https://github.com/shubh73))
335
369
  - Improved Jetpack Compose integration for Expo UI. ([#42450](https://github.com/expo/expo/pull/42450) by [@kudo](https://github.com/kudo))
336
- - [iOS] Added `contentShape` modifier for SwiftUI ([#42813](https://github.com/expo/expo pull/42813) by [@sam-shubham](https://github.com/sam-shubham))
370
+ - [iOS] Added `contentShape` modifier for SwiftUI ([#42813](https://github.com/expo/expo/pull/42813) by [@sam-shubham](https://github.com/sam-shubham))
337
371
 
338
372
  ## 55.0.0-beta.3 — 2026-01-27
339
373
 
package/CLAUDE.md CHANGED
@@ -6,7 +6,7 @@ expo-ui is a library of native UI components for React Native, bridging SwiftUI
6
6
 
7
7
  Bridge native components to JavaScript with as little abstraction as possible. Native views should be thin wrappers — no added logic, state management, or behavior beyond what the platform component provides. Everything that can be set or controlled from JavaScript should be controlled from JavaScript.
8
8
 
9
- Prefer controlled components: state lives in JS and is passed as props, not managed internally by the native view. Use a prop + callback pattern (e.g. `page` + `onPageChange`) instead of imperative ref methods (e.g. `setPage(index)`). This keeps the source of truth in React and makes components predictable and composable.
9
+ Mirror the native API shape. If the underlying SwiftUI / Compose component exposes imperative methods (e.g. SwiftUI's `ScrollViewProxy.scrollTo`, Compose's `PagerState.animateScrollToPage`), expose them as imperative methods in JS don't paper them over with a controlled prop + sync layer just for the sake of a "React-y" API.
10
10
 
11
11
  ## Naming
12
12
 
@@ -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.8'
15
+ version = '56.0.10'
16
16
 
17
17
  android {
18
18
  namespace "expo.modules.ui"
19
19
  defaultConfig {
20
20
  versionCode 1
21
- versionName "56.0.8"
21
+ versionName "56.0.10"
22
22
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23
23
  }
24
24
  buildFeatures {
@@ -2,6 +2,7 @@
2
2
 
3
3
  package expo.modules.ui
4
4
 
5
+ import android.os.Looper
5
6
  import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
6
7
  import androidx.compose.material3.SwitchDefaults
7
8
  import androidx.compose.material3.ToggleButtonDefaults
@@ -39,6 +40,7 @@ import expo.modules.ui.menu.ExposedDropdownMenuBoxContent
39
40
  import expo.modules.ui.menu.ExposedDropdownMenuBoxProps
40
41
  import expo.modules.ui.menu.ExposedDropdownMenuContent
41
42
  import expo.modules.ui.menu.ExposedDropdownMenuProps
43
+ import kotlinx.coroutines.launch
42
44
  import okhttp3.OkHttpClient
43
45
 
44
46
  class ExpoUIModule : Module() {
@@ -79,7 +81,20 @@ class ExpoUIModule : Module() {
79
81
  }
80
82
 
81
83
  Function("setValue") { state: ObservableState, wrapper: Map<String, Any?> ->
82
- state.value = wrapper["value"]
84
+ val newValue = wrapper["value"]
85
+ val mainLooper = Looper.getMainLooper()
86
+ // Update state on the UI thread
87
+ if (mainLooper.isCurrentThread) {
88
+ state.value = newValue
89
+ } else {
90
+ appContext.mainQueue.launch {
91
+ state.value = newValue
92
+ }
93
+ }
94
+ }
95
+
96
+ Function("setOnChange") { state: ObservableState, callback: WorkletCallback? ->
97
+ state.onChange = callback
83
98
  }
84
99
  }
85
100
 
@@ -139,6 +154,8 @@ class ExpoUIModule : Module() {
139
154
  // Class-based views so TooltipBoxView can detect them by type via findChildOfType
140
155
  View(PlainTooltipView::class)
141
156
  View(RichTooltipView::class)
157
+ // Class-based view so SnackbarHostView can read its styling via findChildOfType
158
+ View(SnackbarView::class)
142
159
 
143
160
  //endregion Views use expo-modules-core DSL for uncommon features
144
161
 
@@ -340,6 +357,18 @@ class ExpoUIModule : Module() {
340
357
  }
341
358
  }
342
359
 
360
+ ExpoUIView<LoadingIndicatorProps>("LoadingIndicatorView") {
361
+ Content { props ->
362
+ LoadingIndicatorContent(props)
363
+ }
364
+ }
365
+
366
+ ExpoUIView<ContainedLoadingIndicatorProps>("ContainedLoadingIndicatorView") {
367
+ Content { props ->
368
+ ContainedLoadingIndicatorContent(props)
369
+ }
370
+ }
371
+
343
372
  ExpoUIView<LinearProgressIndicatorProps>("LinearProgressIndicatorView") {
344
373
  Content { props ->
345
374
  LinearProgressIndicatorContent(props)
@@ -429,14 +458,20 @@ class ExpoUIModule : Module() {
429
458
  val scrollToPage by AsyncFunction<Int>()
430
459
  val onCurrentPageChange by Event<HorizontalPagerCurrentPageChangeEvent>()
431
460
  val onSettledPageChange by Event<HorizontalPagerSettledPageChangeEvent>()
461
+ val onPageScroll by Event<HorizontalPagerPageScrollEvent>()
462
+ val onScrollInProgressChange by Event<HorizontalPagerScrollInProgressChangeEvent>()
463
+ val onDragInteraction by Event<HorizontalPagerDragInteractionEvent>()
432
464
 
433
465
  Content { props ->
434
466
  HorizontalPagerContent(
435
- props,
436
- animateScrollToPage,
437
- scrollToPage,
438
- { onCurrentPageChange(it) },
439
- { onSettledPageChange(it) }
467
+ props = props,
468
+ animateScrollToPage = animateScrollToPage,
469
+ scrollToPage = scrollToPage,
470
+ onCurrentPageChange = { onCurrentPageChange(it) },
471
+ onSettledPageChange = { onSettledPageChange(it) },
472
+ onPageScroll = { onPageScroll(it) },
473
+ onScrollInProgressChange = { onScrollInProgressChange(it) },
474
+ onDragInteraction = { onDragInteraction(it) }
440
475
  )
441
476
  }
442
477
  }
@@ -596,6 +631,14 @@ class ExpoUIModule : Module() {
596
631
  }
597
632
  }
598
633
 
634
+ ExpoUIView<SnackbarHostProps>("SnackbarHostView") {
635
+ val showSnackbar by AsyncFunction<SnackbarShowOptions>()
636
+
637
+ Content { props ->
638
+ SnackbarHostContent(props, showSnackbar)
639
+ }
640
+ }
641
+
599
642
  ExpoUIView<TooltipBoxViewProps>("TooltipBoxView") {
600
643
  val show by AsyncFunction()
601
644
  val dismiss by AsyncFunction()
@@ -2,6 +2,7 @@ package expo.modules.ui
2
2
 
3
3
  import android.view.View
4
4
  import android.view.ViewGroup
5
+ import androidx.compose.foundation.interaction.DragInteraction
5
6
  import androidx.compose.foundation.pager.HorizontalPager
6
7
  import androidx.compose.foundation.pager.rememberPagerState
7
8
  import androidx.compose.runtime.Composable
@@ -23,6 +24,7 @@ import expo.modules.kotlin.views.FunctionalComposableScope
23
24
  import expo.modules.kotlin.types.Either
24
25
  import expo.modules.kotlin.types.OptimizedRecord
25
26
  import expo.modules.kotlin.views.OptimizedComposeProps
27
+ import expo.modules.ui.state.WorkletCallback
26
28
 
27
29
  @OptimizedRecord
28
30
  data class HorizontalPagerCurrentPageChangeEvent(
@@ -34,6 +36,22 @@ data class HorizontalPagerSettledPageChangeEvent(
34
36
  @Field val position: Int = 0
35
37
  ) : Record
36
38
 
39
+ @OptimizedRecord
40
+ data class HorizontalPagerPageScrollEvent(
41
+ @Field val currentPage: Int = 0,
42
+ @Field val currentPageOffsetFraction: Float = 0f
43
+ ) : Record
44
+
45
+ @OptimizedRecord
46
+ data class HorizontalPagerScrollInProgressChangeEvent(
47
+ @Field val isScrollInProgress: Boolean = false
48
+ ) : Record
49
+
50
+ @OptimizedRecord
51
+ data class HorizontalPagerDragInteractionEvent(
52
+ @Field val kind: String = "start"
53
+ ) : Record
54
+
37
55
  @OptimizedComposeProps
38
56
  data class HorizontalPagerProps(
39
57
  val initialPage: Int = 0,
@@ -42,6 +60,7 @@ data class HorizontalPagerProps(
42
60
  val userScrollEnabled: Boolean = true,
43
61
  val reverseLayout: Boolean = false,
44
62
  val beyondViewportPageCount: Int = 0,
63
+ val onPageScrollSync: WorkletCallback? = null,
45
64
  val modifiers: ModifierList = emptyList()
46
65
  ) : ComposeProps
47
66
 
@@ -51,7 +70,10 @@ fun FunctionalComposableScope.HorizontalPagerContent(
51
70
  animateScrollToPage: AsyncFunctionHandle<Int>,
52
71
  scrollToPage: AsyncFunctionHandle<Int>,
53
72
  onCurrentPageChange: (HorizontalPagerCurrentPageChangeEvent) -> Unit,
54
- onSettledPageChange: (HorizontalPagerSettledPageChangeEvent) -> Unit
73
+ onSettledPageChange: (HorizontalPagerSettledPageChangeEvent) -> Unit,
74
+ onPageScroll: (HorizontalPagerPageScrollEvent) -> Unit,
75
+ onScrollInProgressChange: (HorizontalPagerScrollInProgressChangeEvent) -> Unit,
76
+ onDragInteraction: (HorizontalPagerDragInteractionEvent) -> Unit
55
77
  ) {
56
78
  // Mirror view.size into snapshot state so the outer scope recomposes when
57
79
  // children are added/removed. Without this, Compose's pager caches its
@@ -59,6 +81,9 @@ fun FunctionalComposableScope.HorizontalPagerContent(
59
81
  // and crashes on scroll past the last index it knew about, because reading
60
82
  // view.size — a plain Java property — registers no snapshot dependency.
61
83
  val pageCountState = remember { mutableIntStateOf(view.size) }
84
+ // Assumes sole ownership of `view`'s OnHierarchyChangeListener — there is
85
+ // only one slot per ViewGroup, and `onDispose` resets it to null. Safe
86
+ // because `view` is the expo Host's private container.
62
87
  DisposableEffect(view) {
63
88
  view.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
64
89
  override fun onChildViewAdded(parent: View?, child: View?) {
@@ -72,35 +97,91 @@ fun FunctionalComposableScope.HorizontalPagerContent(
72
97
  onDispose { view.setOnHierarchyChangeListener(null) }
73
98
  }
74
99
 
75
- val pageCount = pageCountState.intValue
76
- if (pageCount == 0) return
100
+ // Register the imperative handles before the empty-children early return, so
101
+ // in case there ever are JS calls dispatched while `view.size` is still 0, they bind to a handler.
102
+ // `coerceAtLeast(0)` guards against `coerceIn(0, -1)` throwing in that gap.
77
103
  val pagerState = rememberPagerState(
78
- initialPage = props.initialPage.coerceIn(0, pageCount - 1)
104
+ initialPage = props.initialPage.coerceAtLeast(0)
79
105
  ) { pageCountState.intValue }
80
106
  val scope = rememberCoroutineScope()
81
107
 
82
108
  // Dispatch into the composition's scope so the scroll runs with Compose's
83
109
  // MonotonicFrameClock; .join() lets the JS-side promise await completion.
110
+ // Clamp page indices here because Compose throws on out-of-range values.
84
111
  animateScrollToPage.handle { page ->
85
- scope.launch { pagerState.animateScrollToPage(page) }.join()
112
+ val count = pageCountState.intValue
113
+ if (count > 0) {
114
+ val clamped = page.coerceIn(0, count - 1)
115
+ scope.launch { pagerState.animateScrollToPage(clamped) }.join()
116
+ }
86
117
  }
87
118
 
88
119
  scrollToPage.handle { page ->
89
- scope.launch { pagerState.scrollToPage(page) }.join()
120
+ val count = pageCountState.intValue
121
+ if (count > 0) {
122
+ val clamped = page.coerceIn(0, count - 1)
123
+ scope.launch { pagerState.scrollToPage(clamped) }.join()
124
+ }
90
125
  }
91
126
 
92
- // Mirror Compose's PagerState observable fields to JS callbacks. Drop the
93
- // first emission so we don't echo the initial value back on mount.
94
- LaunchedEffect(pagerState) {
95
- snapshotFlow { pagerState.currentPage }
96
- .drop(1)
97
- .collect { onCurrentPageChange(HorizontalPagerCurrentPageChangeEvent(it)) }
98
- }
127
+ val pageCount = pageCountState.intValue
128
+ if (pageCount == 0) return
99
129
 
130
+ // Mirror Compose's PagerState observable fields to JS callbacks. Each
131
+ // state-backed snapshotFlow drops its first emission so we don't echo the
132
+ // initial value back on mount; the interactionSource flow doesn't drop
133
+ // because its emissions are discrete events, not state values.
100
134
  LaunchedEffect(pagerState) {
101
- snapshotFlow { pagerState.settledPage }
102
- .drop(1)
103
- .collect { onSettledPageChange(HorizontalPagerSettledPageChangeEvent(it)) }
135
+ launch {
136
+ snapshotFlow { pagerState.currentPage }
137
+ .drop(1)
138
+ .collect { onCurrentPageChange(HorizontalPagerCurrentPageChangeEvent(it)) }
139
+ }
140
+ launch {
141
+ snapshotFlow { pagerState.settledPage }
142
+ .drop(1)
143
+ .collect { onSettledPageChange(HorizontalPagerSettledPageChangeEvent(it)) }
144
+ }
145
+ launch {
146
+ snapshotFlow {
147
+ pagerState.currentPage to pagerState.currentPageOffsetFraction
148
+ }
149
+ .drop(1)
150
+ .collect { (currentPage, fraction) ->
151
+ // Mutually exclusive: the JS wrapper only wires one path at a time.
152
+ // Skipping the regular event when a worklet is attached avoids the
153
+ // per-frame Record allocation + async JS-thread event dispatch.
154
+ val sync = props.onPageScrollSync
155
+ if (sync != null) {
156
+ sync.invoke(currentPage, fraction)
157
+ } else {
158
+ onPageScroll(
159
+ HorizontalPagerPageScrollEvent(
160
+ currentPage = currentPage,
161
+ currentPageOffsetFraction = fraction
162
+ )
163
+ )
164
+ }
165
+ }
166
+ }
167
+ launch {
168
+ snapshotFlow { pagerState.isScrollInProgress }
169
+ .drop(1)
170
+ .collect {
171
+ onScrollInProgressChange(HorizontalPagerScrollInProgressChangeEvent(isScrollInProgress = it))
172
+ }
173
+ }
174
+ launch {
175
+ pagerState.interactionSource.interactions.collect { interaction ->
176
+ val kind = when (interaction) {
177
+ is DragInteraction.Start -> "start"
178
+ is DragInteraction.Stop -> "stop"
179
+ is DragInteraction.Cancel -> "cancel"
180
+ else -> return@collect
181
+ }
182
+ onDragInteraction(HorizontalPagerDragInteractionEvent(kind = kind))
183
+ }
184
+ }
104
185
  }
105
186
 
106
187
  val contentPadding = props.contentPadding.toPaddingValues()
@@ -0,0 +1,80 @@
1
+ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2
+
3
+ package expo.modules.ui
4
+
5
+ import android.graphics.Color
6
+ import androidx.compose.material3.ContainedLoadingIndicator
7
+ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
8
+ import androidx.compose.material3.LoadingIndicator
9
+ import androidx.compose.material3.LoadingIndicatorDefaults
10
+ import androidx.compose.runtime.Composable
11
+ import expo.modules.kotlin.views.ComposeProps
12
+ import expo.modules.kotlin.views.FunctionalComposableScope
13
+ import expo.modules.kotlin.views.OptimizedComposeProps
14
+ import expo.modules.ui.state.ObservableState
15
+
16
+ // region LoadingIndicator
17
+
18
+ @OptimizedComposeProps
19
+ data class LoadingIndicatorProps(
20
+ val progress: ObservableState? = null,
21
+ val color: Color? = null,
22
+ val modifiers: ModifierList = emptyList()
23
+ ) : ComposeProps
24
+
25
+ @Composable
26
+ fun FunctionalComposableScope.LoadingIndicatorContent(props: LoadingIndicatorProps) {
27
+ val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
28
+ val indicatorColor = props.color.composeOrNull ?: LoadingIndicatorDefaults.indicatorColor
29
+
30
+ val progressState = props.progress
31
+ if (progressState != null) {
32
+ LoadingIndicator(
33
+ progress = { (progressState.value as? Number)?.toFloat() ?: 0f },
34
+ modifier = modifier,
35
+ color = indicatorColor
36
+ )
37
+ } else {
38
+ LoadingIndicator(
39
+ modifier = modifier,
40
+ color = indicatorColor
41
+ )
42
+ }
43
+ }
44
+
45
+ // endregion
46
+
47
+ // region ContainedLoadingIndicator
48
+
49
+ @OptimizedComposeProps
50
+ data class ContainedLoadingIndicatorProps(
51
+ val progress: ObservableState? = null,
52
+ val color: Color? = null,
53
+ val containerColor: Color? = null,
54
+ val modifiers: ModifierList = emptyList()
55
+ ) : ComposeProps
56
+
57
+ @Composable
58
+ fun FunctionalComposableScope.ContainedLoadingIndicatorContent(props: ContainedLoadingIndicatorProps) {
59
+ val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
60
+ val indicatorColor = props.color.composeOrNull ?: LoadingIndicatorDefaults.containedIndicatorColor
61
+ val containerColor = props.containerColor.composeOrNull ?: LoadingIndicatorDefaults.containedContainerColor
62
+
63
+ val progressState = props.progress
64
+ if (progressState != null) {
65
+ ContainedLoadingIndicator(
66
+ progress = { (progressState.value as? Number)?.toFloat() ?: 0f },
67
+ modifier = modifier,
68
+ indicatorColor = indicatorColor,
69
+ containerColor = containerColor
70
+ )
71
+ } else {
72
+ ContainedLoadingIndicator(
73
+ modifier = modifier,
74
+ indicatorColor = indicatorColor,
75
+ containerColor = containerColor
76
+ )
77
+ }
78
+ }
79
+
80
+ // endregion
@@ -3,9 +3,13 @@
3
3
  package expo.modules.ui
4
4
 
5
5
  import android.graphics.Color
6
+ import androidx.compose.animation.animateColorAsState
6
7
  import androidx.compose.animation.animateContentSize
8
+ import androidx.compose.animation.core.AnimationSpec
7
9
  import androidx.compose.animation.core.Spring
10
+ import androidx.compose.animation.core.snap
8
11
  import androidx.compose.animation.core.spring
12
+ import androidx.compose.animation.core.tween
9
13
  import androidx.compose.foundation.BorderStroke
10
14
  import androidx.compose.foundation.ExperimentalFoundationApi
11
15
  import androidx.compose.foundation.background
@@ -38,6 +42,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
38
42
  import androidx.compose.material3.ExposedDropdownMenuAnchorType
39
43
  import androidx.compose.material3.toShape
40
44
  import androidx.compose.runtime.Composable
45
+ import androidx.compose.runtime.getValue
41
46
  import androidx.compose.ui.Modifier
42
47
  import androidx.compose.ui.draw.alpha
43
48
  import androidx.compose.ui.draw.blur
@@ -147,6 +152,24 @@ internal data class BackgroundParams(
147
152
  @Field val color: Color? = null
148
153
  ) : Record
149
154
 
155
+ // Color animation specs reuse the JS `$type` shape from `@expo/ui/jetpack-compose/modifiers/animation` (spring / tween / snap).
156
+ // Keyframes are float-only and aren't supported for colors.
157
+ private fun parseColorAnimationSpec(raw: Any?): AnimationSpec<androidx.compose.ui.graphics.Color>? {
158
+ if (raw !is Map<*, *>) return null
159
+ return when (raw["\$type"]) {
160
+ "spring" -> spring(
161
+ dampingRatio = (raw["dampingRatio"] as? Number)?.toFloat() ?: Spring.DampingRatioNoBouncy,
162
+ stiffness = (raw["stiffness"] as? Number)?.toFloat() ?: Spring.StiffnessMedium
163
+ )
164
+ "tween" -> tween(
165
+ durationMillis = (raw["durationMillis"] as? Number)?.toInt() ?: 300,
166
+ delayMillis = (raw["delayMillis"] as? Number)?.toInt() ?: 0
167
+ )
168
+ "snap" -> snap(delayMillis = (raw["delayMillis"] as? Number)?.toInt() ?: 0)
169
+ else -> null
170
+ }
171
+ }
172
+
150
173
  @OptimizedRecord
151
174
  internal data class BorderParams(
152
175
  @Field val borderWidth: Int = 1,
@@ -457,9 +480,14 @@ object ModifierRegistry {
457
480
  // Appearance modifiers
458
481
  register("background") { map, _, _, _ ->
459
482
  val params = recordFromMap<BackgroundParams>(map)
460
- params.color?.let { color ->
461
- Modifier.background(color.compose)
462
- } ?: Modifier
483
+ val color = params.color?.compose ?: return@register Modifier
484
+ val spec = parseColorAnimationSpec(map["animationSpec"])
485
+ if (spec != null) {
486
+ val animated by animateColorAsState(color, spec, label = "background-color")
487
+ Modifier.background(animated)
488
+ } else {
489
+ Modifier.background(color)
490
+ }
463
491
  }
464
492
 
465
493
  register("border") { map, _, _, _ ->
@@ -0,0 +1,126 @@
1
+ @file:OptIn(ExperimentalMaterial3Api::class)
2
+
3
+ package expo.modules.ui
4
+
5
+ import android.annotation.SuppressLint
6
+ import android.content.Context
7
+ import android.graphics.Color
8
+ import androidx.compose.material3.ExperimentalMaterial3Api
9
+ import androidx.compose.material3.Snackbar
10
+ import androidx.compose.material3.SnackbarDefaults
11
+ import androidx.compose.material3.SnackbarDuration
12
+ import androidx.compose.material3.SnackbarHost
13
+ import androidx.compose.material3.SnackbarHostState
14
+ import androidx.compose.material3.SnackbarResult
15
+ import androidx.compose.runtime.Composable
16
+ import androidx.compose.runtime.MutableState
17
+ import androidx.compose.runtime.mutableStateOf
18
+ import androidx.compose.runtime.remember
19
+ import androidx.compose.runtime.rememberCoroutineScope
20
+ import expo.modules.kotlin.AppContext
21
+ import expo.modules.kotlin.records.Field
22
+ import expo.modules.kotlin.records.Record
23
+ import expo.modules.kotlin.types.OptimizedRecord
24
+ import expo.modules.kotlin.views.AsyncFunctionHandle
25
+ import expo.modules.kotlin.views.ComposableScope
26
+ import expo.modules.kotlin.views.ComposeProps
27
+ import expo.modules.kotlin.views.ExpoComposeView
28
+ import expo.modules.kotlin.views.FunctionalComposableScope
29
+ import expo.modules.kotlin.views.OptimizedComposeProps
30
+ import kotlinx.coroutines.withContext
31
+ import kotlin.coroutines.cancellation.CancellationException
32
+
33
+ // Holds styling props that `SnackbarHost` reads via `findChildOfType`.
34
+
35
+ @OptimizedComposeProps
36
+ data class SnackbarViewProps(
37
+ val containerColor: MutableState<Color?> = mutableStateOf(null),
38
+ val contentColor: MutableState<Color?> = mutableStateOf(null),
39
+ val actionContentColor: MutableState<Color?> = mutableStateOf(null),
40
+ val dismissActionContentColor: MutableState<Color?> = mutableStateOf(null),
41
+ val actionOnNewLine: MutableState<Boolean> = mutableStateOf(false),
42
+ val modifiers: MutableState<ModifierList> = mutableStateOf(emptyList())
43
+ ) : ComposeProps
44
+
45
+ @SuppressLint("ViewConstructor")
46
+ class SnackbarView(context: Context, appContext: AppContext) :
47
+ ExpoComposeView<SnackbarViewProps>(context, appContext) {
48
+ override val props = SnackbarViewProps()
49
+
50
+ @Composable
51
+ override fun ComposableScope.Content() {
52
+ // Empty by design: `SnackbarHost` renders the snackbar using the props above.
53
+ }
54
+ }
55
+
56
+ // --- SnackbarHostView ---
57
+
58
+ @OptimizedRecord
59
+ data class SnackbarShowOptions(
60
+ @Field val message: String = "",
61
+ @Field val actionLabel: String? = null,
62
+ @Field val withDismissAction: Boolean = false,
63
+ @Field val duration: String? = null
64
+ ) : Record
65
+
66
+ @OptimizedComposeProps
67
+ data class SnackbarHostProps(
68
+ val modifiers: ModifierList = emptyList()
69
+ ) : ComposeProps
70
+
71
+ @Composable
72
+ fun FunctionalComposableScope.SnackbarHostContent(
73
+ props: SnackbarHostProps,
74
+ showSnackbar: AsyncFunctionHandle<SnackbarShowOptions>
75
+ ) {
76
+ val hostState = remember { SnackbarHostState() }
77
+ val scope = rememberCoroutineScope()
78
+ val snackbarConfig = findChildOfType<SnackbarView>(view)
79
+
80
+ showSnackbar.handle { options ->
81
+ val duration = when (options.duration) {
82
+ "short" -> SnackbarDuration.Short
83
+ "long" -> SnackbarDuration.Long
84
+ "indefinite" -> SnackbarDuration.Indefinite
85
+ // M3 default: indefinite when there's an action label, else short.
86
+ else -> if (options.actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
87
+ }
88
+ val result = try {
89
+ withContext(scope.coroutineContext) {
90
+ hostState.showSnackbar(
91
+ message = options.message,
92
+ actionLabel = options.actionLabel,
93
+ withDismissAction = options.withDismissAction,
94
+ duration = duration
95
+ )
96
+ }
97
+ } catch (_: CancellationException) {
98
+ // The compose scope can be cancelled if the view is disposed before user dismisses the snackbar or performs the action.
99
+ // In that case, we treat it as if the snackbar was dismissed.
100
+ SnackbarResult.Dismissed
101
+ }
102
+ when (result) {
103
+ SnackbarResult.ActionPerformed -> "actionPerformed"
104
+ SnackbarResult.Dismissed -> "dismissed"
105
+ }
106
+ }
107
+
108
+ SnackbarHost(
109
+ hostState = hostState,
110
+ modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
111
+ ) { data ->
112
+ Snackbar(
113
+ snackbarData = data,
114
+ modifier = snackbarConfig?.let {
115
+ ModifierRegistry.applyModifiers(it.props.modifiers.value, appContext, composableScope, globalEventDispatcher)
116
+ } ?: androidx.compose.ui.Modifier,
117
+ actionOnNewLine = snackbarConfig?.props?.actionOnNewLine?.value ?: false,
118
+ containerColor = snackbarConfig?.props?.containerColor?.value.composeOrNull ?: SnackbarDefaults.color,
119
+ contentColor = snackbarConfig?.props?.contentColor?.value.composeOrNull ?: SnackbarDefaults.contentColor,
120
+ actionContentColor = snackbarConfig?.props?.actionContentColor?.value.composeOrNull
121
+ ?: SnackbarDefaults.actionContentColor,
122
+ dismissActionContentColor = snackbarConfig?.props?.dismissActionContentColor?.value.composeOrNull
123
+ ?: SnackbarDefaults.dismissActionContentColor
124
+ )
125
+ }
126
+ }
@@ -13,6 +13,7 @@ import com.google.android.material.color.utilities.MaterialDynamicColors
13
13
  import com.google.android.material.color.utilities.SchemeTonalSpot
14
14
  import expo.modules.kotlin.records.Field
15
15
  import expo.modules.kotlin.records.Record
16
+ import expo.modules.kotlin.types.OptimizedRecord
16
17
  import expo.modules.ui.ExpoColorScheme
17
18
 
18
19
  /**
@@ -21,6 +22,7 @@ import expo.modules.ui.ExpoColorScheme
21
22
  */
22
23
  internal val isDynamicColorSupported: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
23
24
 
25
+ @OptimizedRecord
24
26
  internal class MaterialColorsOptions : Record {
25
27
  @Field val scheme: ExpoColorScheme? = null
26
28
 
@@ -11,11 +11,21 @@ import expo.modules.kotlin.sharedobjects.SharedObject
11
11
  */
12
12
  class ObservableState(initialValue: Any? = null) : SharedObject() {
13
13
  private val _state: MutableState<Any?> = mutableStateOf(initialValue)
14
+ internal var onChange: WorkletCallback? = null
15
+ private var isNotifying = false
14
16
 
15
17
  var value: Any?
16
18
  get() = _state.value
17
19
  set(v) {
18
20
  _state.value = v
21
+ // Skip re-invoking onChange if state.value was written from inside onChange.
22
+ if (isNotifying) return
23
+ isNotifying = true
24
+ try {
25
+ onChange?.invoke(v)
26
+ } finally {
27
+ isNotifying = false
28
+ }
19
29
  }
20
30
 
21
31
  @Suppress("UNCHECKED_CAST")