@expo/ui 56.0.16 → 56.0.18

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 (133) hide show
  1. package/CHANGELOG.md +33 -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/ModalBottomSheetView.kt +14 -0
  6. package/android/src/main/java/expo/modules/ui/ModifierRegistry.kt +20 -0
  7. package/android/src/main/java/expo/modules/ui/RNHostView.kt +182 -6
  8. package/android/src/main/java/expo/modules/ui/textfield/BasicTextField.kt +203 -0
  9. package/android/src/main/java/expo/modules/ui/{TextFieldView.kt → textfield/TextField.kt} +34 -248
  10. package/android/src/main/java/expo/modules/ui/textfield/TextFieldShared.kt +299 -0
  11. package/build/State/useNativeState.d.ts +8 -3
  12. package/build/State/useNativeState.d.ts.map +1 -1
  13. package/build/community/bottom-sheet/BottomSheet.android.d.ts.map +1 -1
  14. package/build/community/pager-view/PagerView.android.d.ts.map +1 -1
  15. package/build/jetpack-compose/ModalBottomSheet/index.d.ts +6 -0
  16. package/build/jetpack-compose/ModalBottomSheet/index.d.ts.map +1 -1
  17. package/build/jetpack-compose/RNHostView/index.d.ts +8 -0
  18. package/build/jetpack-compose/RNHostView/index.d.ts.map +1 -1
  19. package/build/jetpack-compose/TextField/BasicTextField.d.ts +36 -0
  20. package/build/jetpack-compose/TextField/BasicTextField.d.ts.map +1 -0
  21. package/build/jetpack-compose/TextField/TextField.d.ts +131 -0
  22. package/build/jetpack-compose/TextField/TextField.d.ts.map +1 -0
  23. package/build/jetpack-compose/TextField/index.d.ts +3 -244
  24. package/build/jetpack-compose/TextField/index.d.ts.map +1 -1
  25. package/build/jetpack-compose/TextField/shared.d.ts +171 -0
  26. package/build/jetpack-compose/TextField/shared.d.ts.map +1 -0
  27. package/build/jetpack-compose/index.d.ts +1 -1
  28. package/build/jetpack-compose/index.d.ts.map +1 -1
  29. package/build/jetpack-compose/modifiers/index.d.ts +11 -0
  30. package/build/jetpack-compose/modifiers/index.d.ts.map +1 -1
  31. package/build/swift-ui/Image/index.d.ts +3 -1
  32. package/build/swift-ui/Image/index.d.ts.map +1 -1
  33. package/build/swift-ui/modifiers/index.d.ts +34 -5
  34. package/build/swift-ui/modifiers/index.d.ts.map +1 -1
  35. package/build/swift-ui/modifiers/widgets.d.ts +7 -0
  36. package/build/swift-ui/modifiers/widgets.d.ts.map +1 -1
  37. package/build/universal/Button/index.ios.d.ts.map +1 -1
  38. package/build/universal/FieldGroup/FieldSectionSlots.d.ts.map +1 -1
  39. package/build/universal/FieldGroup/groupChildren.d.ts.map +1 -1
  40. package/build/universal/Text/index.ios.d.ts.map +1 -1
  41. package/build/universal/TextInput/index.android.d.ts.map +1 -1
  42. package/build/universal/TextInput/types.d.ts +5 -1
  43. package/build/universal/TextInput/types.d.ts.map +1 -1
  44. package/build/universal/modifierUtils.d.ts +16 -0
  45. package/build/universal/modifierUtils.d.ts.map +1 -0
  46. package/build/universal/transformStyle.android.d.ts +3 -0
  47. package/build/universal/transformStyle.android.d.ts.map +1 -1
  48. package/build/universal/transformStyle.ios.d.ts +3 -0
  49. package/build/universal/transformStyle.ios.d.ts.map +1 -1
  50. package/build/universal/types.d.ts +2 -0
  51. package/build/universal/types.d.ts.map +1 -1
  52. package/expo-module.config.json +1 -1
  53. package/ios/ImageView.swift +1 -5
  54. package/ios/Modifiers/ImageScaleModifier.swift +29 -0
  55. package/ios/Modifiers/OnGeometryChangeModifier.swift +8 -16
  56. package/ios/Modifiers/ViewModifierRegistry.swift +36 -0
  57. package/ios/Modifiers/WidgetModifiers.swift +12 -0
  58. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.16/expo.modules.ui-56.0.16-sources.jar → 56.0.18/expo.modules.ui-56.0.18-sources.jar} +0 -0
  59. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18-sources.jar.md5 +1 -0
  60. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18-sources.jar.sha1 +1 -0
  61. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18-sources.jar.sha256 +1 -0
  62. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18-sources.jar.sha512 +1 -0
  63. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.aar +0 -0
  64. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.aar.md5 +1 -0
  65. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.aar.sha1 +1 -0
  66. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.aar.sha256 +1 -0
  67. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.aar.sha512 +1 -0
  68. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.16/expo.modules.ui-56.0.16.module → 56.0.18/expo.modules.ui-56.0.18.module} +22 -22
  69. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.module.md5 +1 -0
  70. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.module.sha1 +1 -0
  71. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.module.sha256 +1 -0
  72. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.module.sha512 +1 -0
  73. package/local-maven-repo/expo/modules/ui/expo.modules.ui/{56.0.16/expo.modules.ui-56.0.16.pom → 56.0.18/expo.modules.ui-56.0.18.pom} +1 -1
  74. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.pom.md5 +1 -0
  75. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.pom.sha1 +1 -0
  76. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.pom.sha256 +1 -0
  77. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.18/expo.modules.ui-56.0.18.pom.sha512 +1 -0
  78. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
  79. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
  80. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
  81. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
  82. package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
  83. package/package.json +3 -3
  84. package/src/State/index.fx.ts +4 -1
  85. package/src/State/useNativeState.ts +24 -5
  86. package/src/community/bottom-sheet/BottomSheet.android.tsx +2 -13
  87. package/src/community/bottom-sheet/BottomSheet.ios.tsx +1 -1
  88. package/src/community/datetime-picker/DateTimePicker.tsx +1 -1
  89. package/src/community/menu/MenuView.ios.tsx +1 -1
  90. package/src/community/pager-view/PagerView.android.tsx +21 -3
  91. package/src/community/pager-view/PagerView.ios.tsx +1 -1
  92. package/src/community/picker/Picker.ios.tsx +1 -1
  93. package/src/community/slider/Slider.ios.tsx +1 -1
  94. package/src/jetpack-compose/ModalBottomSheet/index.tsx +7 -0
  95. package/src/jetpack-compose/RNHostView/index.tsx +8 -0
  96. package/src/jetpack-compose/TextField/BasicTextField.tsx +118 -0
  97. package/src/jetpack-compose/TextField/TextField.tsx +198 -0
  98. package/src/jetpack-compose/TextField/index.ts +19 -0
  99. package/src/jetpack-compose/TextField/{index.tsx → shared.ts} +71 -203
  100. package/src/jetpack-compose/index.ts +6 -0
  101. package/src/jetpack-compose/modifiers/index.ts +13 -0
  102. package/src/swift-ui/BottomSheet/index.tsx +1 -1
  103. package/src/swift-ui/Image/index.tsx +12 -3
  104. package/src/swift-ui/modifiers/index.ts +44 -6
  105. package/src/swift-ui/modifiers/widgets.ts +9 -0
  106. package/src/universal/Button/index.ios.tsx +6 -1
  107. package/src/universal/FieldGroup/FieldSectionSlots.tsx +3 -0
  108. package/src/universal/FieldGroup/groupChildren.tsx +3 -0
  109. package/src/universal/Text/index.ios.tsx +3 -1
  110. package/src/universal/TextInput/index.android.tsx +26 -60
  111. package/src/universal/TextInput/types.ts +5 -1
  112. package/src/universal/modifierUtils.ts +23 -0
  113. package/src/universal/transformStyle.android.ts +9 -1
  114. package/src/universal/transformStyle.ios.ts +9 -1
  115. package/src/universal/types.ts +2 -0
  116. package/android/src/main/java/expo/modules/ui/ShadowNodeSyncFlush.kt +0 -28
  117. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.md5 +0 -1
  118. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.sha1 +0 -1
  119. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.sha256 +0 -1
  120. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16-sources.jar.sha512 +0 -1
  121. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar +0 -0
  122. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.md5 +0 -1
  123. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.sha1 +0 -1
  124. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.sha256 +0 -1
  125. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.aar.sha512 +0 -1
  126. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.md5 +0 -1
  127. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.sha1 +0 -1
  128. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.sha256 +0 -1
  129. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.module.sha512 +0 -1
  130. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.md5 +0 -1
  131. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.sha1 +0 -1
  132. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.sha256 +0 -1
  133. package/local-maven-repo/expo/modules/ui/expo.modules.ui/56.0.16/expo.modules.ui-56.0.16.pom.sha512 +0 -1
@@ -1,8 +1,6 @@
1
- package expo.modules.ui
1
+ package expo.modules.ui.textfield
2
2
 
3
3
  import android.graphics.Color
4
- import androidx.compose.foundation.text.KeyboardActions
5
- import androidx.compose.foundation.text.KeyboardOptions
6
4
  import androidx.compose.foundation.text.selection.TextSelectionColors
7
5
  import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
8
6
  import androidx.compose.material3.MaterialExpressiveTheme
@@ -13,35 +11,24 @@ import androidx.compose.material3.TextField
13
11
  import androidx.compose.material3.TextFieldColors
14
12
  import androidx.compose.material3.TextFieldDefaults
15
13
  import androidx.compose.runtime.Composable
16
- import androidx.compose.runtime.LaunchedEffect
17
- import androidx.compose.runtime.mutableStateOf
18
- import androidx.compose.runtime.remember
19
- import androidx.compose.ui.focus.FocusRequester
20
- import androidx.compose.ui.focus.focusRequester
21
- import androidx.compose.ui.focus.onFocusChanged
22
- import androidx.compose.ui.platform.LocalFocusManager
23
- import androidx.compose.ui.text.TextRange
24
- import androidx.compose.ui.text.TextStyle
25
- import androidx.compose.ui.text.input.ImeAction
26
- import androidx.compose.ui.text.input.KeyboardCapitalization
27
- import androidx.compose.ui.text.input.KeyboardType
28
- import androidx.compose.ui.text.input.PasswordVisualTransformation
29
- import androidx.compose.ui.text.input.TextFieldValue
30
- import androidx.compose.ui.text.input.VisualTransformation
31
- import androidx.compose.ui.text.style.TextAlign
32
- import androidx.compose.ui.unit.TextUnit
33
- import androidx.compose.ui.unit.sp
34
14
  import expo.modules.kotlin.records.Field
35
15
  import expo.modules.kotlin.records.Record
36
16
  import expo.modules.kotlin.types.Enumerable
17
+ import expo.modules.kotlin.types.OptimizedRecord
37
18
  import expo.modules.kotlin.views.AsyncFunctionHandle
38
19
  import expo.modules.kotlin.views.AsyncFunctionHandle2
39
20
  import expo.modules.kotlin.views.ComposeProps
40
21
  import expo.modules.kotlin.views.FunctionalComposableScope
41
- import expo.modules.kotlin.types.OptimizedRecord
22
+ import expo.modules.kotlin.views.OptimizedComposeProps
23
+ import expo.modules.ui.GenericEventPayload1
24
+ import expo.modules.ui.ModifierList
25
+ import expo.modules.ui.ShapeRecord
26
+ import expo.modules.ui.composeOrNull
27
+ import expo.modules.ui.findChildSlotView
28
+ import expo.modules.ui.renderSlot
29
+ import expo.modules.ui.shapeFromShapeRecord
42
30
  import expo.modules.ui.state.ObservableState
43
31
  import expo.modules.ui.state.WorkletCallback
44
- import expo.modules.kotlin.views.OptimizedComposeProps
45
32
 
46
33
  // region Records
47
34
 
@@ -50,25 +37,6 @@ enum class TextFieldVariant(val value: String) : Enumerable {
50
37
  OUTLINED("outlined")
51
38
  }
52
39
 
53
- @OptimizedRecord
54
- data class TextFieldKeyboardOptionsRecord(
55
- @Field val capitalization: String? = null,
56
- @Field val autoCorrectEnabled: Boolean? = null,
57
- @Field val keyboardType: String? = null,
58
- @Field val imeAction: String? = null
59
- ) : Record
60
-
61
- @OptimizedRecord
62
- data class TextFieldTextStyleRecord(
63
- @Field val textAlign: TextAlignType? = null,
64
- @Field val color: Color? = null,
65
- @Field val fontSize: Float? = null,
66
- @Field val fontFamily: String? = null,
67
- @Field val fontWeight: TextFontWeight? = null,
68
- @Field val lineHeight: Float? = null,
69
- @Field val letterSpacing: Float? = null
70
- ) : Record
71
-
72
40
  @OptimizedRecord
73
41
  data class TextFieldColorsRecord(
74
42
  // Text
@@ -132,21 +100,6 @@ data class TextFieldSelectionColorsRecord(
132
100
  @Field val backgroundColor: Color? = null
133
101
  ) : Record
134
102
 
135
- data class KeyboardActionEvent(
136
- @Field val action: String,
137
- @Field val value: String
138
- ) : Record
139
-
140
- data class TextFieldSelectionPayload(
141
- @Field val start: Int,
142
- @Field val end: Int
143
- ) : Record
144
-
145
- data class TextFieldValuePayload(
146
- @Field val text: String,
147
- @Field val selection: TextFieldSelectionPayload
148
- ) : Record
149
-
150
103
  // endregion Records
151
104
 
152
105
  // region Color builder
@@ -230,54 +183,6 @@ data class TextFieldProps(
230
183
 
231
184
  // endregion Props
232
185
 
233
- // region Mappers
234
-
235
- private fun String?.toKeyboardType(): KeyboardType = when (this) {
236
- "text" -> KeyboardType.Text
237
- "number" -> KeyboardType.Number
238
- "email" -> KeyboardType.Email
239
- "phone" -> KeyboardType.Phone
240
- "decimal" -> KeyboardType.Decimal
241
- "password" -> KeyboardType.Password
242
- "ascii" -> KeyboardType.Ascii
243
- "uri" -> KeyboardType.Uri
244
- "numberPassword" -> KeyboardType.NumberPassword
245
- else -> KeyboardType.Text
246
- }
247
-
248
- private fun String?.toCapitalization(): KeyboardCapitalization = when (this) {
249
- "characters" -> KeyboardCapitalization.Characters
250
- "none" -> KeyboardCapitalization.None
251
- "sentences" -> KeyboardCapitalization.Sentences
252
- "words" -> KeyboardCapitalization.Words
253
- else -> KeyboardCapitalization.None
254
- }
255
-
256
- private fun String?.toImeAction(): ImeAction = when (this) {
257
- "default" -> ImeAction.Default
258
- "none" -> ImeAction.None
259
- "go" -> ImeAction.Go
260
- "search" -> ImeAction.Search
261
- "send" -> ImeAction.Send
262
- "previous" -> ImeAction.Previous
263
- "next" -> ImeAction.Next
264
- "done" -> ImeAction.Done
265
- else -> ImeAction.Default
266
- }
267
-
268
- // endregion Mappers
269
-
270
- // region Value helpers
271
-
272
- private fun ObservableState.extractSelection(textLength: Int): TextRange {
273
- val selMap = value as? Map<*, *>
274
- val start = (selMap?.get("start") as? Number)?.toInt()?.coerceIn(0, textLength) ?: 0
275
- val end = (selMap?.get("end") as? Number)?.toInt()?.coerceIn(0, textLength) ?: 0
276
- return TextRange(start, end)
277
- }
278
-
279
- // endregion Value helpers
280
-
281
186
  // region View
282
187
 
283
188
  @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -294,31 +199,24 @@ fun FunctionalComposableScope.TextFieldContent(
294
199
  onKeyboardActionTriggered: (KeyboardActionEvent) -> Unit,
295
200
  onSelectionChanged: (TextFieldSelectionPayload) -> Unit
296
201
  ) {
297
- val focusManager = LocalFocusManager.current
298
- val focusRequester = remember { FocusRequester() }
299
- val state = props.value
300
-
301
- setText.handle { text ->
302
- state.value = text
303
- // setText moves the cursor to the end; use setSelection afterwards to override.
304
- props.selection.value = mapOf("start" to text.length, "end" to text.length)
305
- }
306
- focus.handle {
307
- focusRequester.requestFocus()
308
- }
309
- blur.handle {
310
- focusManager.clearFocus()
311
- }
312
- setSelection.handle { start, end ->
313
- val text = state.value as? String ?: ""
314
- val clampedStart = start.coerceIn(0, text.length)
315
- val clampedEnd = end.coerceIn(0, text.length)
316
- props.selection.value = mapOf("start" to clampedStart, "end" to clampedEnd)
317
- }
318
- clear.handle {
319
- state.value = ""
320
- props.selection.value = mapOf("start" to 0, "end" to 0)
321
- }
202
+ val core = rememberTextFieldCore(
203
+ value = props.value,
204
+ selection = props.selection,
205
+ maxLength = props.maxLength,
206
+ autoFocus = props.autoFocus,
207
+ keyboardOptionsRecord = props.keyboardOptions,
208
+ modifiers = props.modifiers,
209
+ onValueChangeSync = props.onValueChangeSync,
210
+ setText = setText,
211
+ setSelection = setSelection,
212
+ clear = clear,
213
+ focus = focus,
214
+ blur = blur,
215
+ onValueChanged = onValueChanged,
216
+ onFocusChange = onFocusChange,
217
+ onKeyboardActionTriggered = onKeyboardActionTriggered,
218
+ onSelectionChanged = onSelectionChanged
219
+ )
322
220
 
323
221
  // Slots
324
222
  val label: (@Composable () -> Unit)? = findChildSlotView(view, "label")?.let { slot -> { slot.renderSlot() } }
@@ -329,58 +227,11 @@ fun FunctionalComposableScope.TextFieldContent(
329
227
  val suffix: (@Composable () -> Unit)? = findChildSlotView(view, "suffix")?.let { slot -> { slot.renderSlot() } }
330
228
  val supportingText: (@Composable () -> Unit)? = findChildSlotView(view, "supportingText")?.let { slot -> { slot.renderSlot() } }
331
229
 
332
- // Keyboard
333
- val kbOpts = props.keyboardOptions
334
- val keyboardOptions = KeyboardOptions.Default.copy(
335
- keyboardType = kbOpts?.keyboardType.toKeyboardType(),
336
- autoCorrectEnabled = kbOpts?.autoCorrectEnabled ?: true,
337
- capitalization = kbOpts?.capitalization.toCapitalization(),
338
- imeAction = kbOpts?.imeAction.toImeAction()
339
- )
340
- val currentText = { state.value as? String ?: "" }
341
- val keyboardActions = KeyboardActions(
342
- onDone = {
343
- defaultKeyboardAction(ImeAction.Done)
344
- onKeyboardActionTriggered(KeyboardActionEvent("done", currentText()))
345
- },
346
- onGo = {
347
- defaultKeyboardAction(ImeAction.Go)
348
- onKeyboardActionTriggered(KeyboardActionEvent("go", currentText()))
349
- },
350
- onNext = {
351
- defaultKeyboardAction(ImeAction.Next)
352
- onKeyboardActionTriggered(KeyboardActionEvent("next", currentText()))
353
- },
354
- onPrevious = {
355
- defaultKeyboardAction(ImeAction.Previous)
356
- onKeyboardActionTriggered(KeyboardActionEvent("previous", currentText()))
357
- },
358
- onSearch = {
359
- defaultKeyboardAction(ImeAction.Search)
360
- onKeyboardActionTriggered(KeyboardActionEvent("search", currentText()))
361
- },
362
- onSend = {
363
- defaultKeyboardAction(ImeAction.Send)
364
- onKeyboardActionTriggered(KeyboardActionEvent("send", currentText()))
365
- }
366
- )
367
-
368
230
  // Lines
369
231
  val singleLine = props.singleLine
370
232
  val maxLines = props.maxLines ?: if (singleLine) 1 else Int.MAX_VALUE
371
233
  val minLines = props.minLines ?: 1
372
234
 
373
- // Modifier
374
- val modifier = ModifierRegistry.applyModifiers(props.modifiers, appContext, composableScope, globalEventDispatcher)
375
- .focusRequester(focusRequester)
376
- .onFocusChanged { focusState ->
377
- onFocusChange(GenericEventPayload1(focusState.isFocused))
378
- }
379
-
380
- if (props.autoFocus) {
381
- LaunchedEffect(Unit) { focusRequester.requestFocus() }
382
- }
383
-
384
235
  val isOutlined = props.variant == TextFieldVariant.OUTLINED
385
236
  val shape = shapeFromShapeRecord(props.shape)
386
237
  ?: if (isOutlined) OutlinedTextFieldDefaults.shape else TextFieldDefaults.shape
@@ -402,73 +253,8 @@ fun FunctionalComposableScope.TextFieldContent(
402
253
  }
403
254
  } ?: baseColors
404
255
 
405
- val text = state.value as? String ?: ""
406
- val selection = props.selection.extractSelection(text.length)
407
-
408
- val localValue = remember { mutableStateOf(TextFieldValue(text, selection)) }
409
- if (localValue.value.text != text || localValue.value.selection != selection) {
410
- localValue.value = TextFieldValue(text, selection)
411
- }
412
-
413
- val value = localValue.value
414
-
415
- val onValueChange: (TextFieldValue) -> Unit = { incoming ->
416
- val new = props.maxLength?.let { max ->
417
- if (incoming.text.length > max) {
418
- val truncated = incoming.text.substring(0, max)
419
- incoming.copy(
420
- text = truncated,
421
- selection = TextRange(
422
- incoming.selection.start.coerceAtMost(max),
423
- incoming.selection.end.coerceAtMost(max)
424
- )
425
- )
426
- } else {
427
- null
428
- }
429
- } ?: incoming
430
- val prev = localValue.value
431
- localValue.value = new
432
- if (new.selection != prev.selection) {
433
- val cur = props.selection.value as? Map<*, *>
434
- val curStart = (cur?.get("start") as? Number)?.toInt()
435
- val curEnd = (cur?.get("end") as? Number)?.toInt()
436
- if (curStart != new.selection.start || curEnd != new.selection.end) {
437
- props.selection.value = mapOf(
438
- "start" to new.selection.start,
439
- "end" to new.selection.end
440
- )
441
- }
442
- onSelectionChanged(TextFieldSelectionPayload(new.selection.start, new.selection.end))
443
- }
444
- if (new.text != prev.text) {
445
- state.value = new.text
446
- val payload = TextFieldValuePayload(
447
- text = new.text,
448
- selection = TextFieldSelectionPayload(new.selection.start, new.selection.end)
449
- )
450
- onValueChanged(payload)
451
- props.onValueChangeSync?.invoke(new.text)
452
- }
453
- }
454
-
455
- val context = appContext.reactContext
456
- val textStyle = props.textStyle?.let { textStyleProps ->
457
- TextStyle(
458
- color = colorToComposeColorOrNull(textStyleProps.color) ?: androidx.compose.ui.graphics.Color.Unspecified,
459
- fontSize = textStyleProps.fontSize?.sp ?: TextUnit.Unspecified,
460
- fontWeight = textStyleProps.fontWeight?.toComposeFontWeight(),
461
- fontFamily = context?.let { resolveFontFamily(textStyleProps.fontFamily, it) },
462
- letterSpacing = textStyleProps.letterSpacing?.sp ?: TextUnit.Unspecified,
463
- lineHeight = textStyleProps.lineHeight?.sp ?: TextUnit.Unspecified,
464
- textAlign = textStyleProps.textAlign?.toComposeTextAlign() ?: TextAlign.Unspecified
465
- )
466
- } ?: TextStyle.Default
467
-
468
- val visualTransformation = when (props.visualTransformation) {
469
- "password" -> PasswordVisualTransformation()
470
- else -> VisualTransformation.None
471
- }
256
+ val textStyle = props.textStyle.toTextStyle(appContext.reactContext)
257
+ val visualTransformation = props.visualTransformation.toVisualTransformation()
472
258
 
473
259
  // Workaround (pending upstream fix, https://issuetracker.google.com/issues/519816993)
474
260
  // the expressive motion scheme's spring overshoots >1f, and TextField's calculateHeight
@@ -477,25 +263,25 @@ fun FunctionalComposableScope.TextFieldContent(
477
263
  MaterialExpressiveTheme(motionScheme = MotionScheme.standard()) {
478
264
  if (isOutlined) {
479
265
  OutlinedTextField(
480
- value = value, onValueChange = onValueChange, modifier = modifier,
266
+ value = core.value, onValueChange = core.onValueChange, modifier = core.modifier,
481
267
  enabled = props.enabled, readOnly = props.readOnly, textStyle = textStyle,
482
268
  label = label, placeholder = placeholder,
483
269
  leadingIcon = leadingIcon, trailingIcon = trailingIcon,
484
270
  prefix = prefix, suffix = suffix, supportingText = supportingText,
485
271
  isError = props.isError, visualTransformation = visualTransformation,
486
- keyboardOptions = keyboardOptions, keyboardActions = keyboardActions,
272
+ keyboardOptions = core.keyboardOptions, keyboardActions = core.keyboardActions,
487
273
  singleLine = singleLine, maxLines = maxLines, minLines = minLines,
488
274
  shape = shape, colors = colors
489
275
  )
490
276
  } else {
491
277
  TextField(
492
- value = value, onValueChange = onValueChange, modifier = modifier,
278
+ value = core.value, onValueChange = core.onValueChange, modifier = core.modifier,
493
279
  enabled = props.enabled, readOnly = props.readOnly, textStyle = textStyle,
494
280
  label = label, placeholder = placeholder,
495
281
  leadingIcon = leadingIcon, trailingIcon = trailingIcon,
496
282
  prefix = prefix, suffix = suffix, supportingText = supportingText,
497
283
  isError = props.isError, visualTransformation = visualTransformation,
498
- keyboardOptions = keyboardOptions, keyboardActions = keyboardActions,
284
+ keyboardOptions = core.keyboardOptions, keyboardActions = core.keyboardActions,
499
285
  singleLine = singleLine, maxLines = maxLines, minLines = minLines,
500
286
  shape = shape, colors = colors
501
287
  )
@@ -0,0 +1,299 @@
1
+ package expo.modules.ui.textfield
2
+
3
+ import android.content.Context
4
+ import android.graphics.Color
5
+ import androidx.compose.foundation.text.KeyboardActions
6
+ import androidx.compose.foundation.text.KeyboardOptions
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.LaunchedEffect
9
+ import androidx.compose.runtime.mutableStateOf
10
+ import androidx.compose.runtime.remember
11
+ import androidx.compose.ui.Modifier
12
+ import androidx.compose.ui.focus.FocusRequester
13
+ import androidx.compose.ui.focus.focusRequester
14
+ import androidx.compose.ui.focus.onFocusChanged
15
+ import androidx.compose.ui.platform.LocalFocusManager
16
+ import androidx.compose.ui.text.TextRange
17
+ import androidx.compose.ui.text.TextStyle
18
+ import androidx.compose.ui.text.input.ImeAction
19
+ import androidx.compose.ui.text.input.KeyboardCapitalization
20
+ import androidx.compose.ui.text.input.KeyboardType
21
+ import androidx.compose.ui.text.input.PasswordVisualTransformation
22
+ import androidx.compose.ui.text.input.TextFieldValue
23
+ import androidx.compose.ui.text.input.VisualTransformation
24
+ import androidx.compose.ui.text.style.TextAlign
25
+ import androidx.compose.ui.unit.TextUnit
26
+ import androidx.compose.ui.unit.sp
27
+ import expo.modules.kotlin.records.Field
28
+ import expo.modules.kotlin.records.Record
29
+ import expo.modules.kotlin.types.OptimizedRecord
30
+ import expo.modules.kotlin.views.AsyncFunctionHandle
31
+ import expo.modules.kotlin.views.AsyncFunctionHandle2
32
+ import expo.modules.kotlin.views.FunctionalComposableScope
33
+ import expo.modules.ui.GenericEventPayload1
34
+ import expo.modules.ui.ModifierList
35
+ import expo.modules.ui.ModifierRegistry
36
+ import expo.modules.ui.TextAlignType
37
+ import expo.modules.ui.TextFontWeight
38
+ import expo.modules.ui.colorToComposeColorOrNull
39
+ import expo.modules.ui.resolveFontFamily
40
+ import expo.modules.ui.state.ObservableState
41
+ import expo.modules.ui.state.WorkletCallback
42
+
43
+ // region Records
44
+
45
+ @OptimizedRecord
46
+ data class TextFieldKeyboardOptionsRecord(
47
+ @Field val capitalization: String? = null,
48
+ @Field val autoCorrectEnabled: Boolean? = null,
49
+ @Field val keyboardType: String? = null,
50
+ @Field val imeAction: String? = null
51
+ ) : Record
52
+
53
+ @OptimizedRecord
54
+ data class TextFieldTextStyleRecord(
55
+ @Field val textAlign: TextAlignType? = null,
56
+ @Field val color: Color? = null,
57
+ @Field val fontSize: Float? = null,
58
+ @Field val fontFamily: String? = null,
59
+ @Field val fontWeight: TextFontWeight? = null,
60
+ @Field val lineHeight: Float? = null,
61
+ @Field val letterSpacing: Float? = null
62
+ ) : Record
63
+
64
+ data class KeyboardActionEvent(
65
+ @Field val action: String,
66
+ @Field val value: String
67
+ ) : Record
68
+
69
+ data class TextFieldSelectionPayload(
70
+ @Field val start: Int,
71
+ @Field val end: Int
72
+ ) : Record
73
+
74
+ data class TextFieldValuePayload(
75
+ @Field val text: String,
76
+ @Field val selection: TextFieldSelectionPayload
77
+ ) : Record
78
+
79
+ // endregion Records
80
+
81
+ // region Mappers
82
+
83
+ private fun String?.toKeyboardType(): KeyboardType = when (this) {
84
+ "text" -> KeyboardType.Text
85
+ "number" -> KeyboardType.Number
86
+ "email" -> KeyboardType.Email
87
+ "phone" -> KeyboardType.Phone
88
+ "decimal" -> KeyboardType.Decimal
89
+ "password" -> KeyboardType.Password
90
+ "ascii" -> KeyboardType.Ascii
91
+ "uri" -> KeyboardType.Uri
92
+ "numberPassword" -> KeyboardType.NumberPassword
93
+ else -> KeyboardType.Text
94
+ }
95
+
96
+ private fun String?.toCapitalization(): KeyboardCapitalization = when (this) {
97
+ "characters" -> KeyboardCapitalization.Characters
98
+ "none" -> KeyboardCapitalization.None
99
+ "sentences" -> KeyboardCapitalization.Sentences
100
+ "words" -> KeyboardCapitalization.Words
101
+ else -> KeyboardCapitalization.None
102
+ }
103
+
104
+ private fun String?.toImeAction(): ImeAction = when (this) {
105
+ "default" -> ImeAction.Default
106
+ "none" -> ImeAction.None
107
+ "go" -> ImeAction.Go
108
+ "search" -> ImeAction.Search
109
+ "send" -> ImeAction.Send
110
+ "previous" -> ImeAction.Previous
111
+ "next" -> ImeAction.Next
112
+ "done" -> ImeAction.Done
113
+ else -> ImeAction.Default
114
+ }
115
+
116
+ internal fun String?.toVisualTransformation(): VisualTransformation = when (this) {
117
+ "password" -> PasswordVisualTransformation()
118
+ else -> VisualTransformation.None
119
+ }
120
+
121
+ internal fun TextFieldTextStyleRecord?.toTextStyle(context: Context?): TextStyle {
122
+ if (this == null) return TextStyle.Default
123
+ return TextStyle(
124
+ color = colorToComposeColorOrNull(color) ?: androidx.compose.ui.graphics.Color.Unspecified,
125
+ fontSize = fontSize?.sp ?: TextUnit.Unspecified,
126
+ fontWeight = fontWeight?.toComposeFontWeight(),
127
+ fontFamily = context?.let { resolveFontFamily(fontFamily, it) },
128
+ letterSpacing = letterSpacing?.sp ?: TextUnit.Unspecified,
129
+ lineHeight = lineHeight?.sp ?: TextUnit.Unspecified,
130
+ textAlign = textAlign?.toComposeTextAlign() ?: TextAlign.Unspecified
131
+ )
132
+ }
133
+
134
+ // endregion Mappers
135
+
136
+ // region Value helpers
137
+
138
+ private fun ObservableState.extractSelection(textLength: Int): TextRange {
139
+ val selMap = value as? Map<*, *>
140
+ val start = (selMap?.get("start") as? Number)?.toInt()?.coerceIn(0, textLength) ?: 0
141
+ val end = (selMap?.get("end") as? Number)?.toInt()?.coerceIn(0, textLength) ?: 0
142
+ return TextRange(start, end)
143
+ }
144
+
145
+ // endregion Value helpers
146
+
147
+ // region Shared core
148
+
149
+ class TextFieldCore(
150
+ val value: TextFieldValue,
151
+ val onValueChange: (TextFieldValue) -> Unit,
152
+ val keyboardOptions: KeyboardOptions,
153
+ val keyboardActions: KeyboardActions,
154
+ val modifier: Modifier
155
+ )
156
+
157
+ @Composable
158
+ fun FunctionalComposableScope.rememberTextFieldCore(
159
+ value: ObservableState,
160
+ selection: ObservableState,
161
+ maxLength: Int?,
162
+ autoFocus: Boolean,
163
+ keyboardOptionsRecord: TextFieldKeyboardOptionsRecord?,
164
+ modifiers: ModifierList,
165
+ onValueChangeSync: WorkletCallback?,
166
+ setText: AsyncFunctionHandle<String>,
167
+ setSelection: AsyncFunctionHandle2<Int, Int>,
168
+ clear: AsyncFunctionHandle<Unit>,
169
+ focus: AsyncFunctionHandle<Unit>,
170
+ blur: AsyncFunctionHandle<Unit>,
171
+ onValueChanged: (TextFieldValuePayload) -> Unit,
172
+ onFocusChange: (GenericEventPayload1<Boolean>) -> Unit,
173
+ onKeyboardActionTriggered: (KeyboardActionEvent) -> Unit,
174
+ onSelectionChanged: (TextFieldSelectionPayload) -> Unit
175
+ ): TextFieldCore {
176
+ val focusManager = LocalFocusManager.current
177
+ val focusRequester = remember { FocusRequester() }
178
+ val state = value
179
+
180
+ setText.handle { text ->
181
+ state.value = text
182
+ // setText moves the cursor to the end; use setSelection afterwards to override.
183
+ selection.value = mapOf("start" to text.length, "end" to text.length)
184
+ }
185
+ focus.handle {
186
+ focusRequester.requestFocus()
187
+ }
188
+ blur.handle {
189
+ focusManager.clearFocus()
190
+ }
191
+ setSelection.handle { start, end ->
192
+ val text = state.value as? String ?: ""
193
+ val clampedStart = start.coerceIn(0, text.length)
194
+ val clampedEnd = end.coerceIn(0, text.length)
195
+ selection.value = mapOf("start" to clampedStart, "end" to clampedEnd)
196
+ }
197
+ clear.handle {
198
+ state.value = ""
199
+ selection.value = mapOf("start" to 0, "end" to 0)
200
+ }
201
+
202
+ // Keyboard
203
+ val keyboardOptions = KeyboardOptions.Default.copy(
204
+ keyboardType = keyboardOptionsRecord?.keyboardType.toKeyboardType(),
205
+ autoCorrectEnabled = keyboardOptionsRecord?.autoCorrectEnabled ?: true,
206
+ capitalization = keyboardOptionsRecord?.capitalization.toCapitalization(),
207
+ imeAction = keyboardOptionsRecord?.imeAction.toImeAction()
208
+ )
209
+ val currentText = { state.value as? String ?: "" }
210
+ val keyboardActions = KeyboardActions(
211
+ onDone = {
212
+ defaultKeyboardAction(ImeAction.Done)
213
+ onKeyboardActionTriggered(KeyboardActionEvent("done", currentText()))
214
+ },
215
+ onGo = {
216
+ defaultKeyboardAction(ImeAction.Go)
217
+ onKeyboardActionTriggered(KeyboardActionEvent("go", currentText()))
218
+ },
219
+ onNext = {
220
+ defaultKeyboardAction(ImeAction.Next)
221
+ onKeyboardActionTriggered(KeyboardActionEvent("next", currentText()))
222
+ },
223
+ onPrevious = {
224
+ defaultKeyboardAction(ImeAction.Previous)
225
+ onKeyboardActionTriggered(KeyboardActionEvent("previous", currentText()))
226
+ },
227
+ onSearch = {
228
+ defaultKeyboardAction(ImeAction.Search)
229
+ onKeyboardActionTriggered(KeyboardActionEvent("search", currentText()))
230
+ },
231
+ onSend = {
232
+ defaultKeyboardAction(ImeAction.Send)
233
+ onKeyboardActionTriggered(KeyboardActionEvent("send", currentText()))
234
+ }
235
+ )
236
+
237
+ // Modifier
238
+ val modifier = ModifierRegistry.applyModifiers(modifiers, appContext, composableScope, globalEventDispatcher)
239
+ .focusRequester(focusRequester)
240
+ .onFocusChanged { focusState ->
241
+ onFocusChange(GenericEventPayload1(focusState.isFocused))
242
+ }
243
+
244
+ if (autoFocus) {
245
+ LaunchedEffect(Unit) { focusRequester.requestFocus() }
246
+ }
247
+
248
+ val text = state.value as? String ?: ""
249
+ val sel = selection.extractSelection(text.length)
250
+
251
+ val localValue = remember { mutableStateOf(TextFieldValue(text, sel)) }
252
+ if (localValue.value.text != text || localValue.value.selection != sel) {
253
+ localValue.value = TextFieldValue(text, sel)
254
+ }
255
+
256
+ val onValueChange: (TextFieldValue) -> Unit = { incoming ->
257
+ val new = maxLength?.let { max ->
258
+ if (incoming.text.length > max) {
259
+ val truncated = incoming.text.substring(0, max)
260
+ incoming.copy(
261
+ text = truncated,
262
+ selection = TextRange(
263
+ incoming.selection.start.coerceAtMost(max),
264
+ incoming.selection.end.coerceAtMost(max)
265
+ )
266
+ )
267
+ } else {
268
+ null
269
+ }
270
+ } ?: incoming
271
+ val prev = localValue.value
272
+ localValue.value = new
273
+ if (new.selection != prev.selection) {
274
+ val cur = selection.value as? Map<*, *>
275
+ val curStart = (cur?.get("start") as? Number)?.toInt()
276
+ val curEnd = (cur?.get("end") as? Number)?.toInt()
277
+ if (curStart != new.selection.start || curEnd != new.selection.end) {
278
+ selection.value = mapOf(
279
+ "start" to new.selection.start,
280
+ "end" to new.selection.end
281
+ )
282
+ }
283
+ onSelectionChanged(TextFieldSelectionPayload(new.selection.start, new.selection.end))
284
+ }
285
+ if (new.text != prev.text) {
286
+ state.value = new.text
287
+ val payload = TextFieldValuePayload(
288
+ text = new.text,
289
+ selection = TextFieldSelectionPayload(new.selection.start, new.selection.end)
290
+ )
291
+ onValueChanged(payload)
292
+ onValueChangeSync?.invoke(new.text)
293
+ }
294
+ }
295
+
296
+ return TextFieldCore(localValue.value, onValueChange, keyboardOptions, keyboardActions, modifier)
297
+ }
298
+
299
+ // endregion Shared core
@@ -12,6 +12,14 @@ export type ObservableState<T> = SharedObject & {
12
12
  * applied. Prefer writing from a worklet when you need synchronous updates
13
13
  */
14
14
  value: T;
15
+ /**
16
+ * Reads the current value. A React Compiler compliant alternative to reading `.value`
17
+ */
18
+ get(): T;
19
+ /**
20
+ * Writes a new value. A React Compiler-compliant alternative to assigning `.value`
21
+ */
22
+ set(value: T): void;
15
23
  /**
16
24
  * A single listener invoked on the native UI runtime whenever the value changes
17
25
  * (after iOS `didSet` and Android's setter). Assigning replaces the previous
@@ -30,9 +38,6 @@ export type ObservableState<T> = SharedObject & {
30
38
  * 'worklet';
31
39
  * console.log('changed to', value);
32
40
  * };
33
- * return () => {
34
- * state.onChange = null;
35
- * };
36
41
  * }, []);
37
42
  * ```
38
43
  */