react-native-controlled-input 0.15.0 → 0.16.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.
@@ -11,7 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.authors = package["author"]
12
12
 
13
13
  s.platforms = { :ios => min_ios_version_supported }
14
- s.source = { :git => "https://github.com/veliseev93/react-native-controlled-input.git", :tag => "#{s.version}" }
14
+ s.source = { :git => "https://github.com/RonasIT/react-native-controlled-input.git", :tag => "#{s.version}" }
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
17
  s.private_header_files = "ios/**/*.h"
package/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 veliseev
3
+ Copyright (c) 2026 Ronas IT
4
+
4
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
5
6
  of this software and associated documentation files (the "Software"), to deal
6
7
  in the Software without restriction, including without limitation the rights
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # react-native-controlled-input
1
+ # @ronas-it/react-native-controlled-input
2
2
 
3
3
  A controlled React Native input that lets you format and constrain the value exactly how you want in JS, while keeping the displayed text in sync without invalid characters flashing in the field.
4
4
 
@@ -8,12 +8,12 @@ With a regular controlled `TextInput`, native input is applied first, then JS re
8
8
 
9
9
  That means invalid characters can still flash in the field for a moment.
10
10
 
11
- `react-native-controlled-input` is built for this exact case: you decide what text is valid, and the displayed value stays driven by `value`.
11
+ `@ronas-it/react-native-controlled-input` is built for this exact case: you decide what text is valid, and the displayed value stays driven by `value`.
12
12
 
13
13
  ## Install
14
14
 
15
15
  ```sh
16
- npm install react-native-controlled-input
16
+ npm install @ronas-it/react-native-controlled-input
17
17
  ```
18
18
 
19
19
  Requires React Native New Architecture / Fabric.
@@ -26,7 +26,7 @@ import { StyleSheet } from 'react-native';
26
26
  import {
27
27
  ControlledInputView,
28
28
  type ControlledInputViewRef,
29
- } from 'react-native-controlled-input';
29
+ } from '@ronas-it/react-native-controlled-input';
30
30
 
31
31
  export function Example() {
32
32
  const [value, setValue] = useState('');
@@ -64,19 +64,21 @@ inputRef.current?.blur();
64
64
 
65
65
  ## Props
66
66
 
67
- | Prop | Type | Description |
68
- |------|------|-------------|
69
- | `value` | `string` | Current input value. |
70
- | `onTextChange` | `(value: string) => void` | Called with the next text value. Filter it and update `value`. |
71
- | `onFocus` | `() => void` | Called when the text input is focused. |
72
- | `onBlur` | `() => void` | Called when the text input is blurred. |
73
- | `autoComplete` | `string` | Specifies autocomplete hints for the system. Same as React Native [`TextInput`](https://reactnative.dev/docs/textinput#autocomplete). |
74
- | `autoCapitalize` | `string` | Can be `none`, `sentences`, `words`, `characters`. Same as React Native [`TextInput`](https://reactnative.dev/docs/textinput#autocapitalize). |
75
- | `keyboardType` | `string` | Determines which keyboard to open, e.g. `numeric`. Same as React Native [`TextInput`](https://reactnative.dev/docs/textinput#keyboardtype). |
76
- | `returnKeyType` | `string` | Determines how the return key should look. Same as React Native [`TextInput`](https://reactnative.dev/docs/textinput#returnkeytype). |
77
- | `placeholder` | `string` | The string that will be rendered before text input has been entered. |
78
- | `placeholderTextColor` | `ColorValue` | The text color of the placeholder string. |
79
- | `selectionColor` | `ColorValue` | The highlight and cursor color of the text input. |
67
+
68
+ | Prop | Type | Description |
69
+ | ---------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
70
+ | `value` | `string` | Current input value. |
71
+ | `onTextChange` | `(value: string) => void` | Called with the next text value. Filter it and update `value`. |
72
+ | `onFocus` | `() => void` | Called when the text input is focused. |
73
+ | `onBlur` | `() => void` | Called when the text input is blurred. |
74
+ | `autoComplete` | `string` | Specifies autocomplete hints for the system. Same as React Native `[TextInput](https://reactnative.dev/docs/textinput#autocomplete)`. |
75
+ | `autoCapitalize` | `string` | Can be `none`, `sentences`, `words`, `characters`. Same as React Native `[TextInput](https://reactnative.dev/docs/textinput#autocapitalize)`. |
76
+ | `keyboardType` | `string` | Determines which keyboard to open, e.g. `numeric`. Same as React Native `[TextInput](https://reactnative.dev/docs/textinput#keyboardtype)`. |
77
+ | `returnKeyType` | `string` | Determines how the return key should look. Same as React Native `[TextInput](https://reactnative.dev/docs/textinput#returnkeytype)`. |
78
+ | `placeholder` | `string` | The string that will be rendered before text input has been entered. |
79
+ | `placeholderTextColor` | `ColorValue` | The text color of the placeholder string. |
80
+ | `selectionColor` | `ColorValue` | The highlight and cursor color of the text input. |
81
+
80
82
 
81
83
  ## Style support
82
84
 
@@ -92,15 +94,570 @@ Commonly used supported styles:
92
94
 
93
95
  Implementation differs internally between platforms, but usage is the same for library consumers.
94
96
 
97
+ ## Fonts
98
+
99
+ In Expo projects, `**fontFamily` on this input only applies when the font is linked for native use**. Relying on runtime loading alone (`useFonts` / `loadAsync`) is often not enough here; use the **expo-font config plugin** so fonts are embedded at build time. See [Expo Font — Configuration in app config](https://docs.expo.dev/versions/latest/sdk/font/#configuration-in-app-config).
100
+
95
101
  ## Ref
96
102
 
97
103
  - `focus()`
98
104
  - `blur()`
99
105
 
106
+ ## Troubleshooting
107
+
108
+ ### `react-native-keyboard-controller`
109
+
110
+ If you use [react-native-keyboard-controller](https://github.com/kirillzyusko/react-native-keyboard-controller) with this package, apply the patch below that matches **your** installed library version so keyboard-aware scrolling and focused-input layout stay correct (especially on Android).
111
+
112
+ `1.20.7` (`react-native-keyboard-controller+1.20.7.patch`)
113
+
114
+ ```diff
115
+ diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt
116
+ index ddd9b88..b8a851b 100644
117
+ --- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt
118
+ +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt
119
+ @@ -8,7 +8,6 @@ import android.view.Gravity
120
+ import android.view.View
121
+ import android.view.ViewTreeObserver.OnPreDrawListener
122
+ import android.widget.EditText
123
+ -import com.facebook.react.views.scroll.ReactScrollView
124
+ import com.facebook.react.views.textinput.ReactEditText
125
+ import com.reactnativekeyboardcontroller.log.Logger
126
+ import java.lang.reflect.Field
127
+ @@ -99,24 +98,7 @@ fun EditText.addOnTextChangedListener(action: (String) -> Unit): TextWatcher {
128
+ }
129
+
130
+ val EditText.parentScrollViewTarget: Int
131
+ - get() {
132
+ - var currentView: View? = this
133
+ -
134
+ - while (currentView != null) {
135
+ - val parentView = currentView.parent as? View
136
+ -
137
+ - if (parentView is ReactScrollView && parentView.scrollEnabled) {
138
+ - // If the parent is a vertical, scrollable ScrollView - return its id
139
+ - return parentView.id
140
+ - }
141
+ -
142
+ - // Move to the next parent view
143
+ - currentView = parentView
144
+ - }
145
+ -
146
+ - // ScrollView was not found
147
+ - return -1
148
+ - }
149
+ + get() = keyboardParentScrollViewTarget()
150
+
151
+ fun EditText?.focus() {
152
+ if (this is ReactEditText) {
153
+ diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ViewKeyboardScrollHost.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ViewKeyboardScrollHost.kt
154
+ new file mode 100644
155
+ index 0000000..75c1ba5
156
+ --- /dev/null
157
+ +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ViewKeyboardScrollHost.kt
158
+ @@ -0,0 +1,35 @@
159
+ +package com.reactnativekeyboardcontroller.extensions
160
+ +
161
+ +import android.view.View
162
+ +import com.facebook.react.views.scroll.ReactScrollView
163
+ +
164
+ +/**
165
+ + * Nearest vertical [ReactScrollView] above this view (same strategy as [EditText.parentScrollViewTarget]).
166
+ + */
167
+ +fun View.keyboardParentScrollViewTarget(): Int {
168
+ + var current: View? = this
169
+ + while (current != null) {
170
+ + val parent = current.parent as? View ?: break
171
+ + if (parent is ReactScrollView && parent.scrollEnabled) {
172
+ + return parent.id
173
+ + }
174
+ + current = parent
175
+ + }
176
+ + return -1
177
+ +}
178
+ +
179
+ +/**
180
+ + * react-native-controlled-input uses Compose [androidx.compose.foundation.text.BasicTextField] without a
181
+ + * platform [android.widget.EditText], so [FocusedInputObserver] never sees `newFocus is EditText`.
182
+ + * Resolve the RN view manager root to measure bounds and [keyboardParentScrollViewTarget].
183
+ + */
184
+ +fun View?.findReactControlledInputHostOrNull(): View? {
185
+ + var v: View? = this
186
+ + while (v != null) {
187
+ + if (v.javaClass.name == "com.controlledinput.ControlledInputView") {
188
+ + return v
189
+ + }
190
+ + v = v.parent as? View
191
+ + }
192
+ + return null
193
+ +}
194
+ diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
195
+ index 1e7be51..373444b 100644
196
+ --- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
197
+ +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
198
+ @@ -19,6 +19,8 @@ import com.reactnativekeyboardcontroller.extensions.addOnTextChangedListener
199
+ import com.reactnativekeyboardcontroller.extensions.dispatchEvent
200
+ import com.reactnativekeyboardcontroller.extensions.dp
201
+ import com.reactnativekeyboardcontroller.extensions.emitEvent
202
+ +import com.reactnativekeyboardcontroller.extensions.findReactControlledInputHostOrNull
203
+ +import com.reactnativekeyboardcontroller.exteыnsions.keyboardParentScrollViewTarget
204
+ import com.reactnativekeyboardcontroller.extensions.parentScrollViewTarget
205
+ import com.reactnativekeyboardcontroller.extensions.rootView
206
+ import com.reactnativekeyboardcontroller.extensions.screenLocation
207
+ @@ -47,6 +49,7 @@ class FocusedInputObserver(
208
+
209
+ // state variables
210
+ private var lastFocusedInput: EditText? = null
211
+ + private var lastComposeHost: View? = null
212
+ private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent
213
+ private var textWatcher: TextWatcher? = null
214
+ private var selectionSubscription: (() -> Unit)? = null
215
+ @@ -101,6 +104,7 @@ class FocusedInputObserver(
216
+ // unfocused or focus was changed
217
+ if (newFocus == null || oldFocus != null) {
218
+ lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
219
+ + lastComposeHost?.removeOnLayoutChangeListener(layoutListener)
220
+ lastFocusedInput?.let { input ->
221
+ val watcher = textWatcher
222
+ // remove it asynchronously to avoid crash in stripe input
223
+ @@ -111,6 +115,7 @@ class FocusedInputObserver(
224
+ }
225
+ selectionSubscription?.invoke()
226
+ lastFocusedInput = null
227
+ + lastComposeHost = null
228
+ }
229
+ if (newFocus is EditText) {
230
+ lastFocusedInput = newFocus
231
+ @@ -130,6 +135,22 @@ class FocusedInputObserver(
232
+ putInt("count", allInputFields.size)
233
+ },
234
+ )
235
+ + } else {
236
+ + val host = newFocus.findReactControlledInputHostOrNull()
237
+ + if (host != null) {
238
+ + lastComposeHost = host
239
+ + host.addOnLayoutChangeListener(layoutListener)
240
+ + this.syncUpLayout()
241
+ +
242
+ + val allInputFields = ViewHierarchyNavigator.getAllInputFields(context?.rootView)
243
+ + context.emitEvent(
244
+ + "KeyboardController::focusDidSet",
245
+ + Arguments.createMap().apply {
246
+ + putInt("current", -1)
247
+ + putInt("count", allInputFields.size)
248
+ + },
249
+ + )
250
+ + }
251
+ }
252
+ // unfocused
253
+ if (newFocus == null) {
254
+ @@ -142,19 +163,37 @@ class FocusedInputObserver(
255
+ }
256
+
257
+ fun syncUpLayout() {
258
+ - val input = lastFocusedInput ?: return
259
+ + val input = lastFocusedInput
260
+ + if (input != null) {
261
+ + val (x, y) = input.screenLocation
262
+ + val event =
263
+ + FocusedInputLayoutChangedEventData(
264
+ + x = input.x.dp,
265
+ + y = input.y.dp,
266
+ + width = input.width.toFloat().dp,
267
+ + height = input.height.toFloat().dp,
268
+ + absoluteX = x.toFloat().dp,
269
+ + absoluteY = y.toFloat().dp,
270
+ + target = input.id,
271
+ + parentScrollViewTarget = input.parentScrollViewTarget,
272
+ + )
273
+ +
274
+ + dispatchEventToJS(event)
275
+ + return
276
+ + }
277
+
278
+ - val (x, y) = input.screenLocation
279
+ + val host = lastComposeHost ?: return
280
+ + val (x, y) = host.screenLocation
281
+ val event =
282
+ FocusedInputLayoutChangedEventData(
283
+ - x = input.x.dp,
284
+ - y = input.y.dp,
285
+ - width = input.width.toFloat().dp,
286
+ - height = input.height.toFloat().dp,
287
+ + x = host.x.dp,
288
+ + y = host.y.dp,
289
+ + width = host.width.toFloat().dp,
290
+ + height = host.height.toFloat().dp,
291
+ absoluteX = x.toFloat().dp,
292
+ absoluteY = y.toFloat().dp,
293
+ - target = input.id,
294
+ - parentScrollViewTarget = input.parentScrollViewTarget,
295
+ + target = host.id,
296
+ + parentScrollViewTarget = host.keyboardParentScrollViewTarget(),
297
+ )
298
+
299
+ dispatchEventToJS(event)
300
+ diff --git a/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx b/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx
301
+ index 4b21666..e35f03c 100644
302
+ --- a/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx
303
+ +++ b/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx
304
+ @@ -246,7 +246,15 @@ const KeyboardAwareScrollView = forwardRef<
305
+
306
+ const customHeight = lastSelection.value?.selection.end.y;
307
+
308
+ - if (!input.value?.layout || !customHeight) {
309
+ + // Without selection geometry (e.g. Compose BasicTextField / controlled-input on Android)
310
+ + // we still must mirror `input` into local `layout` so `maybeScroll` sees parentScrollViewTarget.
311
+ + if (!input.value?.layout) {
312
+ + layout.value = input.value;
313
+ + return false;
314
+ + }
315
+ +
316
+ + if (!customHeight) {
317
+ + layout.value = input.value;
318
+ return false;
319
+ }
320
+
321
+ @@ -351,25 +359,25 @@ const KeyboardAwareScrollView = forwardRef<
322
+ keyboardWillChangeSize ||
323
+ focusWasChanged
324
+ ) {
325
+ - // persist scroll value
326
+ - scrollPosition.value = position.value;
327
+ - // just persist height - later will be used in interpolation
328
+ - keyboardHeight.value = e.height;
329
+ + // Do not clobber scroll / keyboard height while hiding — `keyboardWillHide`
330
+ + // already anchored `scrollPosition` to `scrollBeforeKeyboardMovement`. A bogus
331
+ + // `focusWasChanged` would otherwise zero `keyboardHeight` / wrong scroll anchor,
332
+ + // or run `maybeScroll(0)` and produce an up-then-down jump.
333
+ + if (!keyboardWillHide) {
334
+ + scrollPosition.value = position.value;
335
+ + keyboardHeight.value = e.height;
336
+ + }
337
+ }
338
+
339
+ - // focus was changed
340
+ if (focusWasChanged) {
341
+ tag.value = e.target;
342
+ - // save position of focused text input when keyboard starts to move
343
+ - updateLayoutFromSelection();
344
+ - // save current scroll position - when keyboard will hide we'll reuse
345
+ - // this value to achieve smooth hide effect
346
+ - scrollBeforeKeyboardMovement.value = position.value;
347
+ + if (!keyboardWillHide) {
348
+ + updateLayoutFromSelection();
349
+ + scrollBeforeKeyboardMovement.value = position.value;
350
+ + }
351
+ }
352
+
353
+ - if (focusWasChanged && !keyboardWillAppear.value) {
354
+ - // update position on scroll value, so `onEnd` handler
355
+ - // will pick up correct values
356
+ + if (focusWasChanged && !keyboardWillAppear.value && !keyboardWillHide) {
357
+ position.value += maybeScroll(e.height, true);
358
+ }
359
+ },
360
+ ```
361
+
362
+
363
+
364
+ `1.21.4` (`react-native-keyboard-controller+1.21.4.patch`)
365
+
366
+ ```diff
367
+ diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt
368
+ index ddd9b880..b8a851b5 100644
369
+ --- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt
370
+ +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt
371
+ @@ -8,7 +8,6 @@ import android.view.Gravity
372
+ import android.view.View
373
+ import android.view.ViewTreeObserver.OnPreDrawListener
374
+ import android.widget.EditText
375
+ -import com.facebook.react.views.scroll.ReactScrollView
376
+ import com.facebook.react.views.textinput.ReactEditText
377
+ import com.reactnativekeyboardcontroller.log.Logger
378
+ import java.lang.reflect.Field
379
+ @@ -99,24 +98,7 @@ fun EditText.addOnTextChangedListener(action: (String) -> Unit): TextWatcher {
380
+ }
381
+
382
+ val EditText.parentScrollViewTarget: Int
383
+ - get() {
384
+ - var currentView: View? = this
385
+ -
386
+ - while (currentView != null) {
387
+ - val parentView = currentView.parent as? View
388
+ -
389
+ - if (parentView is ReactScrollView && parentView.scrollEnabled) {
390
+ - // If the parent is a vertical, scrollable ScrollView - return its id
391
+ - return parentView.id
392
+ - }
393
+ -
394
+ - // Move to the next parent view
395
+ - currentView = parentView
396
+ - }
397
+ -
398
+ - // ScrollView was not found
399
+ - return -1
400
+ - }
401
+ + get() = keyboardParentScrollViewTarget()
402
+
403
+ fun EditText?.focus() {
404
+ if (this is ReactEditText) {
405
+ diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ViewKeyboardScrollHost.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ViewKeyboardScrollHost.kt
406
+ new file mode 100644
407
+ index 00000000..75c1ba52
408
+ --- /dev/null
409
+ +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ViewKeyboardScrollHost.kt
410
+ @@ -0,0 +1,35 @@
411
+ +package com.reactnativekeyboardcontroller.extensions
412
+ +
413
+ +import android.view.View
414
+ +import com.facebook.react.views.scroll.ReactScrollView
415
+ +
416
+ +/**
417
+ + * Nearest vertical [ReactScrollView] above this view (same strategy as [EditText.parentScrollViewTarget]).
418
+ + */
419
+ +fun View.keyboardParentScrollViewTarget(): Int {
420
+ + var current: View? = this
421
+ + while (current != null) {
422
+ + val parent = current.parent as? View ?: break
423
+ + if (parent is ReactScrollView && parent.scrollEnabled) {
424
+ + return parent.id
425
+ + }
426
+ + current = parent
427
+ + }
428
+ + return -1
429
+ +}
430
+ +
431
+ +/**
432
+ + * react-native-controlled-input uses Compose [androidx.compose.foundation.text.BasicTextField] without a
433
+ + * platform [android.widget.EditText], so [FocusedInputObserver] never sees `newFocus is EditText`.
434
+ + * Resolve the RN view manager root to measure bounds and [keyboardParentScrollViewTarget].
435
+ + */
436
+ +fun View?.findReactControlledInputHostOrNull(): View? {
437
+ + var v: View? = this
438
+ + while (v != null) {
439
+ + if (v.javaClass.name == "com.controlledinput.ControlledInputView") {
440
+ + return v
441
+ + }
442
+ + v = v.parent as? View
443
+ + }
444
+ + return null
445
+ +}
446
+ diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
447
+ index 4de762da..dfb90a87 100644
448
+ --- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
449
+ +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
450
+ @@ -19,6 +19,8 @@ import com.reactnativekeyboardcontroller.extensions.addOnTextChangedListener
451
+ import com.reactnativekeyboardcontroller.extensions.dispatchEvent
452
+ import com.reactnativekeyboardcontroller.extensions.dp
453
+ import com.reactnativekeyboardcontroller.extensions.emitEvent
454
+ +import com.reactnativekeyboardcontroller.extensions.findReactControlledInputHostOrNull
455
+ +import com.reactnativekeyboardcontroller.extensions.keyboardParentScrollViewTarget
456
+ import com.reactnativekeyboardcontroller.extensions.parentScrollViewTarget
457
+ import com.reactnativekeyboardcontroller.extensions.rootView
458
+ import com.reactnativekeyboardcontroller.extensions.screenLocation
459
+ @@ -47,6 +49,7 @@ class FocusedInputObserver(
460
+
461
+ // state variables
462
+ private var lastFocusedInput: EditText? = null
463
+ + private var lastComposeHost: View? = null
464
+ private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent
465
+ private var textWatcher: TextWatcher? = null
466
+ private var selectionSubscription: (() -> Unit)? = null
467
+ @@ -101,6 +104,7 @@ class FocusedInputObserver(
468
+ // unfocused or focus was changed
469
+ if (newFocus == null || oldFocus != null) {
470
+ lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
471
+ + lastComposeHost?.removeOnLayoutChangeListener(layoutListener)
472
+ lastFocusedInput?.let { input ->
473
+ val watcher = textWatcher
474
+ // remove it asynchronously to avoid crash in stripe input
475
+ @@ -111,6 +115,7 @@ class FocusedInputObserver(
476
+ }
477
+ selectionSubscription?.invoke()
478
+ lastFocusedInput = null
479
+ + lastComposeHost = null
480
+ }
481
+ if (newFocus is EditText) {
482
+ lastFocusedInput = newFocus
483
+ @@ -131,6 +136,22 @@ class FocusedInputObserver(
484
+ putInt("count", allInputFields.size)
485
+ },
486
+ )
487
+ + } else {
488
+ + val host = newFocus.findReactControlledInputHostOrNull()
489
+ + if (host != null) {
490
+ + lastComposeHost = host
491
+ + host.addOnLayoutChangeListener(layoutListener)
492
+ + this.syncUpLayout()
493
+ +
494
+ + val allInputFields = ViewHierarchyNavigator.getAllInputFields(context?.rootView)
495
+ + context.emitEvent(
496
+ + "KeyboardController::focusDidSet",
497
+ + Arguments.createMap().apply {
498
+ + putInt("current", -1)
499
+ + putInt("count", allInputFields.size)
500
+ + },
501
+ + )
502
+ + }
503
+ }
504
+ // unfocused
505
+ if (newFocus == null) {
506
+ @@ -143,19 +164,37 @@ class FocusedInputObserver(
507
+ }
508
+
509
+ fun syncUpLayout() {
510
+ - val input = lastFocusedInput ?: return
511
+ + val input = lastFocusedInput
512
+ + if (input != null) {
513
+ + val (x, y) = input.screenLocation
514
+ + val event =
515
+ + FocusedInputLayoutChangedEventData(
516
+ + x = input.x.dp,
517
+ + y = input.y.dp,
518
+ + width = input.width.toFloat().dp,
519
+ + height = input.height.toFloat().dp,
520
+ + absoluteX = x.toFloat().dp,
521
+ + absoluteY = y.toFloat().dp,
522
+ + target = input.id,
523
+ + parentScrollViewTarget = input.parentScrollViewTarget,
524
+ + )
525
+ +
526
+ + dispatchEventToJS(event)
527
+ + return
528
+ + }
529
+
530
+ - val (x, y) = input.screenLocation
531
+ + val host = lastComposeHost ?: return
532
+ + val (x, y) = host.screenLocation
533
+ val event =
534
+ FocusedInputLayoutChangedEventData(
535
+ - x = input.x.dp,
536
+ - y = input.y.dp,
537
+ - width = input.width.toFloat().dp,
538
+ - height = input.height.toFloat().dp,
539
+ + x = host.x.dp,
540
+ + y = host.y.dp,
541
+ + width = host.width.toFloat().dp,
542
+ + height = host.height.toFloat().dp,
543
+ absoluteX = x.toFloat().dp,
544
+ absoluteY = y.toFloat().dp,
545
+ - target = input.id,
546
+ - parentScrollViewTarget = input.parentScrollViewTarget,
547
+ + target = host.id,
548
+ + parentScrollViewTarget = host.keyboardParentScrollViewTarget(),
549
+ )
550
+
551
+ dispatchEventToJS(event)
552
+ diff --git a/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx b/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx
553
+ index 90586e84..e506d284 100644
554
+ --- a/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx
555
+ +++ b/node_modules/react-native-keyboard-controller/src/components/KeyboardAwareScrollView/index.tsx
556
+ @@ -287,7 +287,15 @@ const KeyboardAwareScrollView = forwardRef<
557
+
558
+ const customHeight = lastSelection.value?.selection.end.y;
559
+
560
+ - if (!input.value?.layout || !customHeight) {
561
+ + // Without selection geometry (e.g. Compose BasicTextField / controlled-input on Android)
562
+ + // we still must mirror `input` into local `layout` so `maybeScroll` sees parentScrollViewTarget.
563
+ + if (!input.value?.layout) {
564
+ + layout.value = input.value;
565
+ + return false;
566
+ + }
567
+ +
568
+ + if (!customHeight) {
569
+ + layout.value = input.value;
570
+ return false;
571
+ }
572
+
573
+ @@ -410,10 +418,14 @@ const KeyboardAwareScrollView = forwardRef<
574
+ keyboardWillChangeSize ||
575
+ focusWasChanged
576
+ ) {
577
+ - // persist scroll value
578
+ - scrollPosition.value = position.value;
579
+ - // just persist height - later will be used in interpolation
580
+ - keyboardHeight.value = e.height;
581
+ + // Do not clobber scroll / keyboard height while hiding — `keyboardWillHide`
582
+ + // already anchored `scrollPosition` to `scrollBeforeKeyboardMovement`. A bogus
583
+ + // `focusWasChanged` would otherwise zero `keyboardHeight` / wrong scroll anchor,
584
+ + // or run `maybeScroll(0)` and produce an up-then-down jump.
585
+ + if (!keyboardWillHide) {
586
+ + scrollPosition.value = position.value;
587
+ + keyboardHeight.value = e.height;
588
+ + }
589
+
590
+ // insets mode: set the full contentInset upfront so that maybeScroll
591
+ // calculations are correct from the very first onMove frame.
592
+ @@ -428,33 +440,35 @@ const KeyboardAwareScrollView = forwardRef<
593
+ if (focusWasChanged) {
594
+ tag.value = e.target;
595
+
596
+ - if (
597
+ - lastSelection.value?.target === e.target &&
598
+ - selectionUpdatedSinceHide.value
599
+ - ) {
600
+ - // fresh selection arrived before onStart - use it to update layout
601
+ - updateLayoutFromSelection();
602
+ - pendingSelectionForFocus.value = false;
603
+ - } else {
604
+ - // selection hasn't arrived yet for the new target (iOS 15),
605
+ - // or it's stale from previous session (Android refocus same input).
606
+ - // Use stale selection as best-effort fallback if available for same target,
607
+ - // otherwise fall back to full input layout.
608
+ - // Will be corrected if a fresh onSelectionChange arrives.
609
+ - if (lastSelection.value?.target === e.target) {
610
+ + if (!keyboardWillHide) {
611
+ + if (
612
+ + lastSelection.value?.target === e.target &&
613
+ + selectionUpdatedSinceHide.value
614
+ + ) {
615
+ + // fresh selection arrived before onStart - use it to update layout
616
+ updateLayoutFromSelection();
617
+ - } else if (input.value) {
618
+ - layout.value = input.value;
619
+ + pendingSelectionForFocus.value = false;
620
+ + } else {
621
+ + // selection hasn't arrived yet for the new target (iOS 15),
622
+ + // or it's stale from previous session (Android refocus same input).
623
+ + // Use stale selection as best-effort fallback if available for same target,
624
+ + // otherwise fall back to full input layout.
625
+ + // Will be corrected if a fresh onSelectionChange arrives.
626
+ + if (lastSelection.value?.target === e.target) {
627
+ + updateLayoutFromSelection();
628
+ + } else if (input.value) {
629
+ + layout.value = input.value;
630
+ + }
631
+ + pendingSelectionForFocus.value = true;
632
+ }
633
+ - pendingSelectionForFocus.value = true;
634
+ - }
635
+
636
+ - // save current scroll position - when keyboard will hide we'll reuse
637
+ - // this value to achieve smooth hide effect
638
+ - scrollBeforeKeyboardMovement.value = position.value;
639
+ + // save current scroll position - when keyboard will hide we'll reuse
640
+ + // this value to achieve smooth hide effect
641
+ + scrollBeforeKeyboardMovement.value = position.value;
642
+ + }
643
+ }
644
+
645
+ - if (focusWasChanged && !keyboardWillAppear.value) {
646
+ + if (focusWasChanged && !keyboardWillAppear.value && !keyboardWillHide) {
647
+ if (!pendingSelectionForFocus.value) {
648
+ // update position on scroll value, so `onEnd` handler
649
+ // will pick up correct values
650
+ position.value += maybeScroll(e.height, true);
651
+ }
652
+ }
653
+ ```
654
+
655
+
656
+
100
657
  ## License
101
658
 
102
659
  MIT
103
660
 
104
661
  ---
105
662
 
106
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
663
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -4,6 +4,7 @@ import android.content.Context
4
4
  import android.util.AttributeSet
5
5
  import android.view.View
6
6
  import android.view.inputmethod.InputMethodManager
7
+ import android.widget.EditText
7
8
  import android.widget.LinearLayout
8
9
  import androidx.annotation.UiThread
9
10
  import androidx.compose.runtime.LaunchedEffect
@@ -26,8 +27,7 @@ import com.facebook.react.uimanager.UIManagerHelper
26
27
  import kotlinx.coroutines.flow.MutableStateFlow
27
28
 
28
29
  /**
29
- * RN + Jetpack Compose hosting view aligned with Expo UI / [ExpoComposeView] + [ExpoView]:
30
- * - [shouldUseAndroidLayout]: requestLayout posts measureAndLayout (RN #17968)
30
+ * - [shouldUseAndroidLayout]: requestLayout posts measureAndLayout
31
31
  * - onMeasure skips child [ComposeView] until attached (window + WindowRecomposer)
32
32
  *
33
33
  * @see expo.modules.kotlin.views.ExpoComposeView
@@ -60,11 +60,48 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
60
60
  private var usesLocalFallbackLifecycle = false
61
61
  private var windowLifecycleBound = false
62
62
 
63
- /** Same role as ExpoComposeView(withHostingView = true) → ExpoView.shouldUseAndroidLayout */
63
+ /**
64
+ * Invisible EditText that acts as a focus proxy for react-native-keyboard-controller.
65
+ *
66
+ * keyboard-controller's FocusedInputObserver only tracks views that are `EditText` instances.
67
+ * Since ControlledInputView uses Compose BasicTextField, it is invisible to that observer.
68
+ * Focusing this proxy when Compose gains focus makes keyboard-controller aware of the input
69
+ * and allows KeyboardAwareScrollView to scroll correctly.
70
+ *
71
+ * Layout height is 0 so LinearLayout ignores it visually. onLayout() forces its bounds to
72
+ * match ControlledInputView so keyboard-controller reads the correct width/height/position.
73
+ */
74
+ private val focusProxy: EditText by lazy {
75
+ EditText(context).also { proxy ->
76
+ proxy.layoutParams = LayoutParams(0, 0)
77
+ proxy.visibility = View.INVISIBLE
78
+ proxy.isFocusableInTouchMode = true
79
+ proxy.showSoftInputOnFocus = false
80
+ proxy.isClickable = false
81
+ proxy.isCursorVisible = false
82
+ proxy.isLongClickable = false
83
+ }
84
+ }
85
+
86
+ private fun requestFocusProxy() {
87
+ focusProxy.requestFocus()
88
+ }
89
+
90
+ private fun clearFocusProxy() {
91
+ focusProxy.clearFocus()
92
+ }
93
+
64
94
  private val shouldUseAndroidLayout = true
65
95
 
96
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
97
+ super.onLayout(changed, l, t, r, b)
98
+ // Force proxy bounds to match ControlledInputView so keyboard-controller reads
99
+ // the correct width/height/absolutePosition when the proxy is focused.
100
+ focusProxy.layout(0, 0, width, height)
101
+ }
102
+
66
103
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
67
- // ExpoComposeView.onMeasure — do not measure ComposeView until attached to a window.
104
+ // Do not measure ComposeView until attached to a window.
68
105
  if (shouldUseAndroidLayout && !isAttachedToWindow) {
69
106
  setMeasuredDimension(
70
107
  MeasureSpec.getSize(widthMeasureSpec).coerceAtLeast(0),
@@ -75,10 +112,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
75
112
  super.onMeasure(widthMeasureSpec, heightMeasureSpec)
76
113
  }
77
114
 
78
- /**
79
- * ExpoView.requestLayout / measureAndLayout — Fabric/Yoga often won't drive Android layout
80
- * for native children; this mirrors expo-modules-core behavior.
81
- */
115
+ // Fabric/Yoga often won't drive Android layout for native children.
82
116
  override fun requestLayout() {
83
117
  super.requestLayout()
84
118
  if (shouldUseAndroidLayout) {
@@ -149,6 +183,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
149
183
  val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
150
184
  imm.hideSoftInputFromWindow(windowToken, 0)
151
185
  clearFocus()
186
+ clearFocusProxy()
152
187
  }
153
188
 
154
189
  fun focus() {
@@ -167,6 +202,8 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
167
202
 
168
203
  viewModel = JetpackComposeViewModel()
169
204
 
205
+ addView(focusProxy)
206
+
170
207
  composeView = ComposeView(context).also { cv ->
171
208
  cv.layoutParams = LayoutParams(
172
209
  LayoutParams.MATCH_PARENT,
@@ -227,6 +264,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
227
264
  viewId
228
265
  )
229
266
  )
267
+ requestFocusProxy()
230
268
  },
231
269
  onBlur = {
232
270
  val surfaceId = UIManagerHelper.getSurfaceId(context)
@@ -239,6 +277,7 @@ class ControlledInputView : LinearLayout, LifecycleOwner {
239
277
  viewId
240
278
  )
241
279
  )
280
+ clearFocusProxy()
242
281
  },
243
282
  focusRequester = focusRequester
244
283
  )
@@ -101,13 +101,11 @@ using namespace facebook::react;
101
101
  - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
102
102
  {
103
103
  if ([commandName isEqualToString:@"focus"]) {
104
- NSLog(@"[ControlledInputView] handleCommand focus");
105
104
  [_inputView focus];
106
105
  return;
107
106
  }
108
107
 
109
108
  if ([commandName isEqualToString:@"blur"]) {
110
- NSLog(@"[ControlledInputView] handleCommand blur");
111
109
  [_inputView blur];
112
110
  return;
113
111
  }
@@ -148,8 +148,7 @@ public class RNControlledInput: UIView, UITextFieldDelegate {
148
148
  textField.font = UIFont.systemFont(ofSize: fontSize)
149
149
  return
150
150
  }
151
- // UIFont(name:size:) needs the PostScript name. Expo's useFonts keys are aliased via a
152
- // swizzled UIFont.fontNames(forFamilyName:) — RN Text resolves aliases that way, so we must too.
151
+
153
152
  if let font = UIFont(name: family, size: fontSize) {
154
153
  textField.font = font
155
154
  return
@@ -60,4 +60,4 @@ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
60
60
  supportedCommands: ['focus', 'blur'],
61
61
  });
62
62
 
63
- export default codegenNativeComponent<NativeProps>('ControlledInputView');
63
+ export default codegenNativeComponent<NativeProps>('ControlledInputView');
@@ -2,8 +2,6 @@
2
2
 
3
3
  import { forwardRef, memo, useImperativeHandle, useLayoutEffect, useRef } from 'react';
4
4
  import { Platform, processColor, StyleSheet } from 'react-native';
5
- // TextInputState is not exported from the public react-native package.
6
- // eslint-disable-next-line @react-native/no-deep-imports -- integrate with ScrollView keyboard dismissal
7
5
  import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState';
8
6
  import ControlledInputViewNativeComponent from './ControlledInputViewNativeComponent';
9
7
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -1 +1 @@
1
- {"version":3,"names":["forwardRef","memo","useImperativeHandle","useLayoutEffect","useRef","Platform","processColor","StyleSheet","TextInputState","ControlledInputViewNativeComponent","jsx","_jsx","androidComposeHandledKeys","resolveAndroidComposeViewPadding","flat","padding","paddingTop","paddingVertical","paddingBottom","paddingLeft","paddingHorizontal","paddingRight","ControlledInputView","style","onTextChange","onFocus","onBlur","selectionColor","placeholderTextColor","rest","ref","nativeRef","isNativePlatform","OS","node","current","registerInput","unregisterInput","currentlyFocusedInput","blurTextInput","flattenedStyle","flatten","viewStyle","inputStyle","Object","fromEntries","entries","filter","k","includes","hasPadding","some","v","color","fontSize","fontFamily","borderWidth","borderRadius","borderColor","backgroundColor","iosViewStyle","hasTextStyle","undefined","handleTextChange","e","nativeEvent","value","handleFocus","focusInput","handleBlur","blurInput","blur","focus","focusTextInput","displayName"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["forwardRef","memo","useImperativeHandle","useLayoutEffect","useRef","Platform","processColor","StyleSheet","TextInputState","ControlledInputViewNativeComponent","jsx","_jsx","androidComposeHandledKeys","resolveAndroidComposeViewPadding","flat","padding","paddingTop","paddingVertical","paddingBottom","paddingLeft","paddingHorizontal","paddingRight","ControlledInputView","style","onTextChange","onFocus","onBlur","selectionColor","placeholderTextColor","rest","ref","nativeRef","isNativePlatform","OS","node","current","registerInput","unregisterInput","currentlyFocusedInput","blurTextInput","flattenedStyle","flatten","viewStyle","inputStyle","Object","fromEntries","entries","filter","k","includes","hasPadding","some","v","color","fontSize","fontFamily","borderWidth","borderRadius","borderColor","backgroundColor","iosViewStyle","hasTextStyle","undefined","handleTextChange","e","nativeEvent","value","handleFocus","focusInput","handleBlur","blurInput","blur","focus","focusTextInput","displayName"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,cAAc,EAGpB,MAAM,cAAc,CAAC;AAItB,OAA2C,EACzC,KAAK,WAAW,EAEjB,MAAM,sCAAsC,CAAC;AAE9C,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,KAAK,uBAAuB,GAAG,IAAI,CACjC,cAAc,EACZ,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,aAAa,GACb,sBAAsB,GACtB,gBAAgB,CACnB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,IAAI,CACzC,WAAW,EACX,YAAY,GAAG,cAAc,GAAG,MAAM,uBAAuB,CAC9D,GACC,uBAAuB,GAAG;IACxB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,CAAC;AAsCJ,eAAO,MAAM,mBAAmB;mBAvCb,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI;0DA0KzC,CAAC;AAIF,cAAc,sCAAsC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,cAAc,EAGpB,MAAM,cAAc,CAAC;AAEtB,OAA2C,EACzC,KAAK,WAAW,EAEjB,MAAM,sCAAsC,CAAC;AAE9C,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED,KAAK,uBAAuB,GAAG,IAAI,CACjC,cAAc,EACZ,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,eAAe,GACf,aAAa,GACb,sBAAsB,GACtB,gBAAgB,CACnB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,IAAI,CACzC,WAAW,EACX,YAAY,GAAG,cAAc,GAAG,MAAM,uBAAuB,CAC9D,GACC,uBAAuB,GAAG;IACxB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,CAAC;AA2CJ,eAAO,MAAM,mBAAmB;mBA5Cb,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI;0DAiLzC,CAAC;AAIF,cAAc,sCAAsC,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-controlled-input",
3
- "version": "0.15.0",
4
- "description": "React Native Controlled Inputative Controlled Input",
3
+ "version": "0.16.0",
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",
7
7
  "exports": {
@@ -32,13 +32,15 @@
32
32
  "!**/.*"
33
33
  ],
34
34
  "scripts": {
35
- "example": "yarn workspace react-native-controlled-input-example",
35
+ "example": "npm run start -w react-native-controlled-input-example",
36
36
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
37
37
  "prepare": "bob build",
38
+ "build": "bob build",
38
39
  "typecheck": "tsc",
39
- "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "lint": "npx tsc && npx eslint ./",
41
+ "format": "npx prettier --write . && npm run lint -- --fix",
40
42
  "test": "jest",
41
- "release": "release-it --only-version"
43
+ "release": "release-it"
42
44
  },
43
45
  "keywords": [
44
46
  "react-native",
@@ -49,7 +51,7 @@
49
51
  "type": "git",
50
52
  "url": "git+https://github.com/veliseev93/react-native-controlled-input.git"
51
53
  },
52
- "author": "veliseev <veliseev@ronasit.com> (https://github.com/veliseev93)",
54
+ "author": "veliseev93 (https://github.com/veliseev93)",
53
55
  "license": "MIT",
54
56
  "bugs": {
55
57
  "url": "https://github.com/veliseev93/react-native-controlled-input/issues"
@@ -60,28 +62,30 @@
60
62
  },
61
63
  "devDependencies": {
62
64
  "@commitlint/config-conventional": "^19.8.1",
63
- "@eslint/compat": "^1.3.2",
64
- "@eslint/eslintrc": "^3.3.1",
65
- "@eslint/js": "^9.35.0",
65
+ "@eslint/compat": "^1.2.5",
66
66
  "@react-native/babel-preset": "0.83.0",
67
- "@react-native/eslint-config": "0.83.0",
68
67
  "@release-it/conventional-changelog": "^10.0.1",
68
+ "@stylistic/eslint-plugin": "^2.13.0",
69
69
  "@types/jest": "^29.5.14",
70
70
  "@types/react": "^19.2.0",
71
71
  "commitlint": "^19.8.1",
72
72
  "del-cli": "^6.0.0",
73
- "eslint": "^9.35.0",
74
- "eslint-config-prettier": "^10.1.8",
75
- "eslint-plugin-prettier": "^5.5.4",
73
+ "eslint": "^9.18.0",
74
+ "eslint-config-prettier": "^9.1.0",
75
+ "eslint-import-resolver-typescript": "^3.7.0",
76
+ "eslint-plugin-import": "^2.31.0",
77
+ "eslint-plugin-unused-imports": "^4.1.4",
78
+ "globals": "^15.14.0",
76
79
  "jest": "^29.7.0",
77
80
  "lefthook": "^2.0.3",
78
- "prettier": "^2.8.8",
81
+ "prettier": "^3.4.2",
79
82
  "react": "19.2.0",
80
83
  "react-native": "0.83.0",
81
84
  "react-native-builder-bob": "^0.40.17",
82
85
  "release-it": "^19.0.4",
83
86
  "turbo": "^2.5.6",
84
- "typescript": "^5.9.2"
87
+ "typescript": "^5.7.3",
88
+ "typescript-eslint": "^8.20.0"
85
89
  },
86
90
  "peerDependencies": {
87
91
  "react": "*",
@@ -90,7 +94,6 @@
90
94
  "workspaces": [
91
95
  "example"
92
96
  ],
93
- "packageManager": "yarn@4.11.0",
94
97
  "react-native-builder-bob": {
95
98
  "source": "src",
96
99
  "output": "lib",
@@ -122,13 +125,6 @@
122
125
  }
123
126
  }
124
127
  },
125
- "prettier": {
126
- "quoteProps": "consistent",
127
- "singleQuote": true,
128
- "tabWidth": 2,
129
- "trailingComma": "es5",
130
- "useTabs": false
131
- },
132
128
  "jest": {
133
129
  "preset": "react-native",
134
130
  "modulePathIgnorePatterns": [
@@ -142,12 +138,15 @@
142
138
  ]
143
139
  },
144
140
  "release-it": {
141
+ "increment": false,
145
142
  "git": {
146
143
  "commitMessage": "chore: release ${version}",
147
- "tagName": "v${version}"
144
+ "tagName": "v${version}",
145
+ "requireBranch": ["main", "test-keboard--controller"]
148
146
  },
149
147
  "npm": {
150
- "publish": true
148
+ "publish": true,
149
+ "tag": "alpha"
151
150
  },
152
151
  "github": {
153
152
  "release": true
@@ -60,4 +60,4 @@ export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
60
60
  supportedCommands: ['focus', 'blur'],
61
61
  });
62
62
 
63
- export default codegenNativeComponent<NativeProps>('ControlledInputView');
63
+ export default codegenNativeComponent<NativeProps>('ControlledInputView');
package/src/index.tsx CHANGED
@@ -14,8 +14,6 @@ import {
14
14
  type ViewStyle,
15
15
  type TextStyle,
16
16
  } from 'react-native';
17
- // TextInputState is not exported from the public react-native package.
18
- // eslint-disable-next-line @react-native/no-deep-imports -- integrate with ScrollView keyboard dismissal
19
17
  import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState';
20
18
  import ControlledInputViewNativeComponent, {
21
19
  type NativeProps,
@@ -71,7 +69,12 @@ const androidComposeHandledKeys = [
71
69
  'backgroundColor',
72
70
  ];
73
71
 
74
- function resolveAndroidComposeViewPadding(flat: Record<string, any>) {
72
+ function resolveAndroidComposeViewPadding(flat: Record<string, any>): {
73
+ paddingTop: unknown;
74
+ paddingBottom: unknown;
75
+ paddingLeft: unknown;
76
+ paddingRight: unknown;
77
+ } {
75
78
  const padding = flat.padding ?? 0;
76
79
 
77
80
  return {
@@ -108,6 +111,7 @@ export const ControlledInputView = memo(
108
111
  }
109
112
 
110
113
  const node = nativeRef.current;
114
+
111
115
  if (node == null) {
112
116
  return;
113
117
  }
@@ -116,6 +120,7 @@ export const ControlledInputView = memo(
116
120
 
117
121
  return () => {
118
122
  TextInputState.unregisterInput(node);
123
+
119
124
  if (TextInputState.currentlyFocusedInput() === node) {
120
125
  TextInputState.blurTextInput(node);
121
126
  }
@@ -167,20 +172,20 @@ export const ControlledInputView = memo(
167
172
 
168
173
  const handleTextChange = (e: {
169
174
  nativeEvent: Readonly<TextChangeEvent>;
170
- }) => {
175
+ }): void => {
171
176
  if (onTextChange) {
172
177
  onTextChange(e.nativeEvent.value);
173
178
  }
174
179
  };
175
180
 
176
- const handleFocus = (e: ControlledInputFocusEvent) => {
181
+ const handleFocus = (e: ControlledInputFocusEvent): void => {
177
182
  if (isNativePlatform) {
178
183
  TextInputState.focusInput(nativeRef.current);
179
184
  }
180
185
  onFocus?.(e);
181
186
  };
182
187
 
183
- const handleBlur = (e: ControlledInputBlurEvent) => {
188
+ const handleBlur = (e: ControlledInputBlurEvent): void => {
184
189
  if (isNativePlatform) {
185
190
  TextInputState.blurInput(nativeRef.current);
186
191
  }
@@ -15,6 +15,7 @@ declare module 'react-native/Libraries/Types/CodegenTypes' {
15
15
 
16
16
  declare module 'react-native/Libraries/Utilities/codegenNativeComponent' {
17
17
  import type { HostComponent } from 'react-native';
18
+
18
19
  export default function codegenNativeComponent<T>(
19
20
  componentName: string
20
21
  ): HostComponent<T>;
@@ -34,4 +35,4 @@ declare module 'react-native/Libraries/Components/TextInput/TextInputState' {
34
35
  };
35
36
 
36
37
  export default TextInputState;
37
- }
38
+ }