react-native-controlled-input 0.18.0 → 0.19.0

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.
@@ -27,6 +27,7 @@ import androidx.savedstate.SavedStateRegistryOwner
27
27
  import androidx.savedstate.setViewTreeSavedStateRegistryOwner
28
28
  import com.facebook.react.bridge.ReactContext
29
29
  import com.facebook.react.uimanager.UIManagerHelper
30
+ import com.facebook.react.uimanager.events.Event
30
31
  import kotlinx.coroutines.flow.MutableStateFlow
31
32
 
32
33
  /**
@@ -171,72 +172,137 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
171
172
  }
172
173
 
173
174
  /**
174
- * Uses reflection to restore KBC's lastFocusedInput = focusProxy and call syncUpLayout(),
175
- * so that KeyboardAwareScrollView receives the correct absoluteY / height to scroll to.
176
- *
177
- * Chain: EdgeToEdgeViewRegistry.get() → .callback → .layoutObserver → .lastFocusedInput / .syncUpLayout()
175
+ * EdgeToEdgeViewRegistry.get() callback FocusedInputObserver.
176
+ * Null if react-native-keyboard-controller is missing or not initialized.
178
177
  */
179
- private fun restoreKbcTracking() {
180
- Log.d(TAG, "restoreKbcTracking: starting reflection chain")
178
+ private fun resolveKbcFocusedInputObserver(): Any? {
181
179
  try {
182
- // 1. EdgeToEdgeViewRegistry is a Kotlin object — access via INSTANCE field
183
180
  val registryClass =
184
181
  Class.forName("com.reactnativekeyboardcontroller.views.EdgeToEdgeViewRegistry")
185
182
  val registryInstance = registryClass.getField("INSTANCE").get(null)
186
183
  val edgeToEdgeView =
187
184
  registryClass.getDeclaredMethod("get").invoke(registryInstance)
188
185
  ?: run {
189
- Log.w(TAG, "restoreKbcTracking: EdgeToEdgeViewRegistry.get() == null")
190
- return
186
+ Log.w(TAG, "resolveKbcFocusedInputObserver: EdgeToEdgeViewRegistry.get() == null")
187
+ return null
191
188
  }
192
189
 
193
- // 2. callback: KeyboardAnimationCallback (internal var — find by type)
194
190
  val callbackField =
195
191
  edgeToEdgeView.javaClass.declaredFields.firstOrNull {
196
192
  it.type.simpleName == "KeyboardAnimationCallback"
197
193
  }
198
194
  ?: run {
199
- Log.w(TAG, "restoreKbcTracking: KeyboardAnimationCallback field not found")
200
- return
195
+ Log.w(TAG, "resolveKbcFocusedInputObserver: KeyboardAnimationCallback field not found")
196
+ return null
201
197
  }
202
198
  callbackField.isAccessible = true
203
199
  val callback =
204
200
  callbackField.get(edgeToEdgeView)
205
201
  ?: run {
206
- Log.w(TAG, "restoreKbcTracking: callback == null")
207
- return
202
+ Log.w(TAG, "resolveKbcFocusedInputObserver: callback == null")
203
+ return null
208
204
  }
209
205
 
210
- // 3. layoutObserver: FocusedInputObserver (internal var — find by type)
211
206
  val observerField =
212
207
  callback.javaClass.declaredFields.firstOrNull {
213
208
  it.type.simpleName == "FocusedInputObserver"
214
209
  }
215
210
  ?: run {
216
- Log.w(TAG, "restoreKbcTracking: FocusedInputObserver field not found")
217
- return
211
+ Log.w(TAG, "resolveKbcFocusedInputObserver: FocusedInputObserver field not found")
212
+ return null
218
213
  }
219
214
  observerField.isAccessible = true
220
- val observer =
221
- observerField.get(callback)
215
+ return observerField.get(callback)
216
+ ?: run {
217
+ Log.w(TAG, "resolveKbcFocusedInputObserver: layoutObserver == null")
218
+ null
219
+ }
220
+ } catch (_: ClassNotFoundException) {
221
+ Log.d(TAG, "resolveKbcFocusedInputObserver: keyboard-controller not on classpath")
222
+ return null
223
+ } catch (e: Exception) {
224
+ Log.w(TAG, "resolveKbcFocusedInputObserver: ${e.javaClass.simpleName}: ${e.message}")
225
+ return null
226
+ }
227
+ }
228
+
229
+ /** Approximate one line height in dp for JS customHeight when proxy has no Layout yet. */
230
+ private fun approximateSelectionEndYDp(): Double {
231
+ viewModel.inputStyle.value?.fontSize?.toDouble()?.takeIf { it > 0 }?.let { return it }
232
+ val dm = resources.displayMetrics
233
+ return (focusProxy.textSize / dm.density).toDouble().coerceAtLeast(12.0)
234
+ }
235
+
236
+ /**
237
+ * Synthetic topFocusedInputSelectionChanged without a compile dependency on KBC.
238
+ * Helps older KeyboardAwareScrollView when proxy never gets android.text.Layout in time.
239
+ */
240
+ private fun dispatchSyntheticKbcSelectionEvent(observer: Any) {
241
+ val reactContext = context as? ReactContext ?: return
242
+ try {
243
+ val epField = observer.javaClass.getDeclaredField("eventPropagationView")
244
+ epField.isAccessible = true
245
+ val propagationId = (epField.get(observer) as View).id
246
+
247
+ val surfaceId = UIManagerHelper.getSurfaceId(this)
248
+ val targetId = id
249
+ val endY = approximateSelectionEndYDp()
250
+
251
+ val dataClz =
252
+ Class.forName("com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEventData")
253
+ val dataCtor =
254
+ dataClz.declaredConstructors.singleOrNull { it.parameterTypes.size == 7 }
222
255
  ?: run {
223
- Log.w(TAG, "restoreKbcTracking: layoutObserver == null")
256
+ Log.w(TAG, "dispatchSyntheticKbcSelectionEvent: no 7-arg data ctor")
224
257
  return
225
258
  }
259
+ dataCtor.isAccessible = true
260
+ val data =
261
+ dataCtor.newInstance(targetId, 0.0, 0.0, 0.0, endY, 0, 0)
262
+
263
+ val eventClz =
264
+ Class.forName("com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEvent")
265
+ val eventCtor =
266
+ eventClz.getConstructor(
267
+ Int::class.javaPrimitiveType,
268
+ Int::class.javaPrimitiveType,
269
+ dataClz,
270
+ )
271
+ val event = eventCtor.newInstance(surfaceId, propagationId, data) as Event<*>
272
+
273
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, propagationId)
274
+ ?.dispatchEvent(event)
275
+ Log.d(
276
+ TAG,
277
+ "dispatchSyntheticKbcSelectionEvent: propagationId=$propagationId target=$targetId endY(dp)=$endY",
278
+ )
279
+ } catch (e: Exception) {
280
+ Log.w(TAG, "dispatchSyntheticKbcSelectionEvent: ${e.javaClass.simpleName}: ${e.message}")
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Reflection: lastFocusedInput = focusProxy, syncUpLayout(), then synthetic selection
286
+ * so JS gets customHeight (selection.end.y) without waiting for KeyboardControllerSelectionWatcher.
287
+ */
288
+ private fun restoreKbcTracking() {
289
+ Log.d(TAG, "restoreKbcTracking: starting reflection chain")
290
+ try {
291
+ val observer = resolveKbcFocusedInputObserver() ?: return
226
292
 
227
- // 4. Set lastFocusedInput = focusProxy (private var)
228
293
  val lastFocusedField = observer.javaClass.getDeclaredField("lastFocusedInput")
229
294
  lastFocusedField.isAccessible = true
230
295
  lastFocusedField.set(observer, focusProxy)
231
296
  Log.d(TAG, "restoreKbcTracking: lastFocusedInput set to focusProxy")
232
297
 
233
- // 5. Call syncUpLayout() — public fun, dispatches FocusedInputLayoutChangedEvent to JS
234
298
  val syncMethod = observer.javaClass.getDeclaredMethod("syncUpLayout")
235
299
  syncMethod.isAccessible = true
236
300
  syncMethod.invoke(observer)
237
- Log.d(TAG, "restoreKbcTracking: syncUpLayout() invoked — KBC layout event sent to JS")
301
+ Log.d(TAG, "restoreKbcTracking: syncUpLayout() invoked")
302
+
303
+ dispatchSyntheticKbcSelectionEvent(observer)
238
304
  } catch (e: Exception) {
239
- Log.w(TAG, "restoreKbcTracking: ${e.javaClass.simpleName}: ${e.message}")
305
+ Log.w(TAG, "restoreKbcTracking: ${e.javaClass.simpleName}: ${e.message}")
240
306
  }
241
307
  }
242
308
 
@@ -417,7 +483,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
417
483
  )
418
484
  },
419
485
  onFocus = {
420
- Log.d(TAG, "━━ Compose onFocus ━━ id=$id isRestoring=$isRestoringComposeFocus")
486
+ Log.d(TAG, "Compose onFocus id=$id isRestoring=$isRestoringComposeFocus")
421
487
  if (isRestoringComposeFocus) {
422
488
  // Second onFocus triggered by focusRequester.requestFocus() inside the restoration
423
489
  // dance — KBC is being synced via reflection, keyboard is showing.
@@ -435,7 +501,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
435
501
  }
436
502
  },
437
503
  onBlur = {
438
- Log.d(TAG, "━━ Compose onBlur ━━ id=$id proxyFocused=${focusProxy.isFocused}")
504
+ Log.d(TAG, "Compose onBlur id=$id proxyFocused=${focusProxy.isFocused}")
439
505
  if (focusProxy.isFocused) {
440
506
  // Proxy stole Android focus from AndroidComposeView → this blur is synthetic.
441
507
  // Restore Compose focus so the keyboard stays/re-appears.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-controlled-input",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "React Native controlled TextInput with strict value sync (Fabric)",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",