react-native-keyboard-controller 1.10.5 → 1.11.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.
Files changed (115) hide show
  1. package/README.md +1 -0
  2. package/android/build.gradle +6 -0
  3. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +4 -0
  4. package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt +2 -0
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +22 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +9 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/{InteractiveKeyboardProvider.kt → interactive/InteractiveKeyboardProvider.kt} +1 -1
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/{KeyboardAnimationController.kt → interactive/KeyboardAnimationController.kt} +1 -1
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/{interpolators → interactive/interpolators}/Interpolator.kt +1 -1
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/{interpolators → interactive/interpolators}/IosInterpolator.kt +1 -1
  11. package/android/src/main/java/com/reactnativekeyboardcontroller/{interpolators → interactive/interpolators}/LinearInterpolator.kt +1 -1
  12. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +20 -0
  13. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +8 -14
  14. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +15 -0
  15. package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/FocusedInputHolder.kt +16 -0
  16. package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt +101 -0
  17. package/android/src/main/java/com/reactnativekeyboardcontroller/views/KeyboardGestureAreaReactViewGroup.kt +4 -4
  18. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +5 -0
  19. package/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorTest.kt +188 -0
  20. package/ios/.swiftlint.yml +4 -0
  21. package/ios/Extensions.swift +29 -0
  22. package/ios/KeyboardController.xcodeproj/project.pbxproj +20 -0
  23. package/ios/KeyboardControllerModule.mm +18 -0
  24. package/ios/events/FocusedInputLayoutChangedEvent.m +3 -0
  25. package/ios/observers/FocusedInputObserver.swift +20 -4
  26. package/ios/observers/KeyboardMovementObserver.swift +5 -4
  27. package/ios/traversal/FocusedInputHolder.swift +32 -0
  28. package/ios/traversal/TextInput.swift +26 -0
  29. package/ios/traversal/ViewHierarchyNavigator.swift +116 -0
  30. package/ios/views/KeyboardControllerView.mm +10 -0
  31. package/ios/views/KeyboardControllerViewManager.swift +4 -1
  32. package/jest/index.js +20 -0
  33. package/lib/commonjs/bindings.js +12 -1
  34. package/lib/commonjs/bindings.js.map +1 -1
  35. package/lib/commonjs/bindings.native.js +11 -2
  36. package/lib/commonjs/bindings.native.js.map +1 -1
  37. package/lib/commonjs/components/KeyboardAwareScrollView/index.js +18 -6
  38. package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
  39. package/lib/commonjs/components/KeyboardToolbar/Arrow.js +79 -0
  40. package/lib/commonjs/components/KeyboardToolbar/Arrow.js.map +1 -0
  41. package/lib/commonjs/components/KeyboardToolbar/Button.js +74 -0
  42. package/lib/commonjs/components/KeyboardToolbar/Button.js.map +1 -0
  43. package/lib/commonjs/components/KeyboardToolbar/colors.js +41 -0
  44. package/lib/commonjs/components/KeyboardToolbar/colors.js.map +1 -0
  45. package/lib/commonjs/components/KeyboardToolbar/index.js +136 -0
  46. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -0
  47. package/lib/commonjs/components/hooks/useColorScheme.js +11 -0
  48. package/lib/commonjs/components/hooks/useColorScheme.js.map +1 -0
  49. package/lib/commonjs/components/hooks/useKeyboardInterpolation.js +3 -2
  50. package/lib/commonjs/components/hooks/useKeyboardInterpolation.js.map +1 -1
  51. package/lib/commonjs/components/index.js +21 -0
  52. package/lib/commonjs/components/index.js.map +1 -1
  53. package/lib/commonjs/index.js +22 -13
  54. package/lib/commonjs/index.js.map +1 -1
  55. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  56. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  57. package/lib/commonjs/types.js.map +1 -1
  58. package/lib/module/bindings.js +10 -0
  59. package/lib/module/bindings.js.map +1 -1
  60. package/lib/module/bindings.native.js +9 -1
  61. package/lib/module/bindings.native.js.map +1 -1
  62. package/lib/module/components/KeyboardAwareScrollView/index.js +18 -6
  63. package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
  64. package/lib/module/components/KeyboardToolbar/Arrow.js +69 -0
  65. package/lib/module/components/KeyboardToolbar/Arrow.js.map +1 -0
  66. package/lib/module/components/KeyboardToolbar/Button.js +64 -0
  67. package/lib/module/components/KeyboardToolbar/Button.js.map +1 -0
  68. package/lib/module/components/KeyboardToolbar/colors.js +34 -0
  69. package/lib/module/components/KeyboardToolbar/colors.js.map +1 -0
  70. package/lib/module/components/KeyboardToolbar/index.js +121 -0
  71. package/lib/module/components/KeyboardToolbar/index.js.map +1 -0
  72. package/lib/module/components/hooks/useColorScheme.js +4 -0
  73. package/lib/module/components/hooks/useColorScheme.js.map +1 -0
  74. package/lib/module/components/hooks/useKeyboardInterpolation.js +3 -2
  75. package/lib/module/components/hooks/useKeyboardInterpolation.js.map +1 -1
  76. package/lib/module/components/index.js +1 -0
  77. package/lib/module/components/index.js.map +1 -1
  78. package/lib/module/index.js +3 -2
  79. package/lib/module/index.js.map +1 -1
  80. package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  81. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  82. package/lib/module/types.js.map +1 -1
  83. package/lib/typescript/bindings.d.ts +6 -1
  84. package/lib/typescript/bindings.native.d.ts +6 -1
  85. package/lib/typescript/components/KeyboardToolbar/Arrow.d.ts +9 -0
  86. package/lib/typescript/components/KeyboardToolbar/Button.d.ts +15 -0
  87. package/lib/typescript/components/KeyboardToolbar/colors.d.ts +17 -0
  88. package/lib/typescript/components/KeyboardToolbar/index.d.ts +25 -0
  89. package/lib/typescript/components/hooks/useColorScheme.d.ts +2 -0
  90. package/lib/typescript/components/index.d.ts +1 -0
  91. package/lib/typescript/index.d.ts +1 -2
  92. package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +1 -0
  93. package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -0
  94. package/lib/typescript/types.d.ts +11 -0
  95. package/package.json +5 -1
  96. package/src/bindings.native.ts +11 -1
  97. package/src/bindings.ts +9 -0
  98. package/src/components/KeyboardAwareScrollView/index.tsx +22 -2
  99. package/src/components/KeyboardToolbar/Arrow.tsx +80 -0
  100. package/src/components/KeyboardToolbar/Button.tsx +98 -0
  101. package/src/components/KeyboardToolbar/colors.ts +51 -0
  102. package/src/components/KeyboardToolbar/index.tsx +154 -0
  103. package/src/components/hooks/useColorScheme.ts +5 -0
  104. package/src/components/hooks/useKeyboardInterpolation.ts +3 -2
  105. package/src/components/index.ts +5 -0
  106. package/src/index.ts +4 -1
  107. package/src/specs/KeyboardControllerViewNativeComponent.ts +1 -0
  108. package/src/specs/NativeKeyboardController.ts +1 -0
  109. package/src/types.ts +14 -0
  110. package/lib/commonjs/replicas.js +0 -125
  111. package/lib/commonjs/replicas.js.map +0 -1
  112. package/lib/module/replicas.js +0 -115
  113. package/lib/module/replicas.js.map +0 -1
  114. package/lib/typescript/replicas.d.ts +0 -38
  115. package/src/replicas.ts +0 -154
package/README.md CHANGED
@@ -14,6 +14,7 @@ Keyboard manager which works in identical way on both iOS and Android.
14
14
  - reanimated support 🚀
15
15
  - interactive keyboard dismissing 👆📱
16
16
  - prebuilt components (`KeyboardStickyView`, `KeyboardAwareScrollView`, re-worked `KeyboardAvoidingView`) 📚
17
+ - `KeyboardToolbar` with easy behavior customization of _**previous**_, _**next**_ and _**done**_ buttons in the keyboard toolbar 📐
17
18
  - easy focused input information retrieval 📝 🔮
18
19
  - works with any navigation library 🧭
19
20
  - and more is coming... Stay tuned! 😊
@@ -170,4 +170,10 @@ dependencies {
170
170
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
171
171
  implementation 'androidx.core:core-ktx:1.5.0-beta03'
172
172
  implementation 'androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03'
173
+
174
+ // dependencies for testing
175
+ testImplementation 'junit:junit:4.13.2'
176
+ testImplementation 'org.robolectric:robolectric:4.11.1'
177
+ testImplementation 'androidx.test:core:1.5.0'
178
+ testImplementation "androidx.test:core-ktx:1.5.0"
173
179
  }
@@ -20,6 +20,10 @@ class KeyboardControllerModule(mReactContext: ReactApplicationContext) : NativeK
20
20
  module.dismiss()
21
21
  }
22
22
 
23
+ override fun setFocusTo(direction: String) {
24
+ module.setFocusTo(direction)
25
+ }
26
+
23
27
  override fun addListener(eventName: String?) {
24
28
  /* Required for RN built-in Event Emitter Calls. */
25
29
  }
@@ -12,6 +12,7 @@ data class FocusedInputLayoutChangedEventData(
12
12
  val absoluteX: Double,
13
13
  val absoluteY: Double,
14
14
  val target: Int,
15
+ val parentScrollViewTarget: Int,
15
16
  )
16
17
 
17
18
  class FocusedInputLayoutChangedEvent(
@@ -26,6 +27,7 @@ class FocusedInputLayoutChangedEvent(
26
27
 
27
28
  override fun getEventData(): WritableMap? = Arguments.createMap().apply {
28
29
  putInt("target", event.target)
30
+ putInt("parentScrollViewTarget", event.parentScrollViewTarget)
29
31
  putMap(
30
32
  "layout",
31
33
  Arguments.createMap().apply {
@@ -3,7 +3,9 @@ package com.reactnativekeyboardcontroller.extensions
3
3
  import android.text.Editable
4
4
  import android.text.TextWatcher
5
5
  import android.util.Log
6
+ import android.view.View
6
7
  import android.widget.EditText
8
+ import android.widget.ScrollView
7
9
  import com.facebook.react.views.textinput.ReactEditText
8
10
  import java.lang.reflect.Field
9
11
 
@@ -64,3 +66,23 @@ fun EditText.addOnTextChangedListener(action: (String) -> Unit): TextWatcher {
64
66
 
65
67
  return listener
66
68
  }
69
+
70
+ val EditText.parentScrollViewTarget: Int
71
+ get() {
72
+ var currentView: View? = this
73
+
74
+ while (currentView != null) {
75
+ val parentView = currentView.parent as? View
76
+
77
+ if (parentView is ScrollView) {
78
+ // If the parent is a ScrollView, return its id
79
+ return parentView.id
80
+ }
81
+
82
+ // Move to the next parent view
83
+ currentView = parentView
84
+ }
85
+
86
+ // ScrollView was not found
87
+ return -1
88
+ }
@@ -1,6 +1,9 @@
1
1
  package com.reactnativekeyboardcontroller.extensions
2
2
 
3
+ import android.util.Log
3
4
  import android.view.View
5
+ import com.facebook.react.bridge.WritableMap
6
+ import com.facebook.react.modules.core.DeviceEventManagerModule
4
7
  import com.facebook.react.uimanager.ThemedReactContext
5
8
  import com.facebook.react.uimanager.UIManagerHelper
6
9
  import com.facebook.react.uimanager.events.Event
@@ -14,3 +17,9 @@ fun ThemedReactContext?.dispatchEvent(viewId: Int, event: Event<*>) {
14
17
  UIManagerHelper.getEventDispatcherForReactTag(this, viewId)
15
18
  eventDispatcher?.dispatchEvent(event)
16
19
  }
20
+
21
+ fun ThemedReactContext?.emitEvent(event: String, params: WritableMap) {
22
+ this?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params)
23
+
24
+ Log.i("ThemedReactContext", event)
25
+ }
@@ -1,4 +1,4 @@
1
- package com.reactnativekeyboardcontroller
1
+ package com.reactnativekeyboardcontroller.interactive
2
2
 
3
3
  object InteractiveKeyboardProvider {
4
4
  var shown = false
@@ -1,4 +1,4 @@
1
- package com.reactnativekeyboardcontroller
1
+ package com.reactnativekeyboardcontroller.interactive
2
2
 
3
3
  import android.os.CancellationSignal
4
4
  import android.view.View
@@ -1,4 +1,4 @@
1
- package com.reactnativekeyboardcontroller.interpolators
1
+ package com.reactnativekeyboardcontroller.interactive.interpolators
2
2
 
3
3
  interface Interpolator {
4
4
  /**
@@ -1,4 +1,4 @@
1
- package com.reactnativekeyboardcontroller.interpolators
1
+ package com.reactnativekeyboardcontroller.interactive.interpolators
2
2
 
3
3
  class IosInterpolator : Interpolator {
4
4
  override fun interpolate(
@@ -1,4 +1,4 @@
1
- package com.reactnativekeyboardcontroller.interpolators
1
+ package com.reactnativekeyboardcontroller.interactive.interpolators
2
2
 
3
3
  class LinearInterpolator : Interpolator {
4
4
  override fun interpolate(
@@ -3,6 +3,7 @@ package com.reactnativekeyboardcontroller.listeners
3
3
  import android.text.TextWatcher
4
4
  import android.view.View.OnLayoutChangeListener
5
5
  import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
6
+ import com.facebook.react.bridge.Arguments
6
7
  import com.facebook.react.uimanager.ThemedReactContext
7
8
  import com.facebook.react.uimanager.UIManagerHelper
8
9
  import com.facebook.react.views.textinput.ReactEditText
@@ -13,7 +14,12 @@ import com.reactnativekeyboardcontroller.events.FocusedInputTextChangedEvent
13
14
  import com.reactnativekeyboardcontroller.extensions.addOnTextChangedListener
14
15
  import com.reactnativekeyboardcontroller.extensions.dispatchEvent
15
16
  import com.reactnativekeyboardcontroller.extensions.dp
17
+ import com.reactnativekeyboardcontroller.extensions.emitEvent
18
+ import com.reactnativekeyboardcontroller.extensions.parentScrollViewTarget
19
+ import com.reactnativekeyboardcontroller.extensions.rootView
16
20
  import com.reactnativekeyboardcontroller.extensions.screenLocation
21
+ import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
22
+ import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator
17
23
 
18
24
  val noFocusedInputEvent = FocusedInputLayoutChangedEventData(
19
25
  x = 0.0,
@@ -23,6 +29,7 @@ val noFocusedInputEvent = FocusedInputLayoutChangedEventData(
23
29
  absoluteX = 0.0,
24
30
  absoluteY = 0.0,
25
31
  target = -1,
32
+ parentScrollViewTarget = -1,
26
33
  )
27
34
 
28
35
  class FocusedInputObserver(val view: ReactViewGroup, private val context: ThemedReactContext?) {
@@ -62,6 +69,18 @@ class FocusedInputObserver(val view: ReactViewGroup, private val context: Themed
62
69
  newFocus.addOnLayoutChangeListener(layoutListener)
63
70
  this.syncUpLayout()
64
71
  textWatcher = newFocus.addOnTextChangedListener(textListener)
72
+ FocusedInputHolder.set(newFocus)
73
+
74
+ val allInputFields = ViewHierarchyNavigator.getAllInputFields(context?.rootView)
75
+ val currentIndex = allInputFields.indexOf(newFocus)
76
+
77
+ context.emitEvent(
78
+ "KeyboardController::focusDidSet",
79
+ Arguments.createMap().apply {
80
+ putInt("current", currentIndex)
81
+ putInt("count", allInputFields.size)
82
+ },
83
+ )
65
84
  }
66
85
  // unfocused
67
86
  if (newFocus == null) {
@@ -85,6 +104,7 @@ class FocusedInputObserver(val view: ReactViewGroup, private val context: Themed
85
104
  absoluteX = x.toFloat().dp,
86
105
  absoluteY = y.toFloat().dp,
87
106
  target = input.id,
107
+ parentScrollViewTarget = input.parentScrollViewTarget,
88
108
  )
89
109
 
90
110
  dispatchEventToJS(event)
@@ -13,16 +13,16 @@ import androidx.core.view.WindowInsetsAnimationCompat
13
13
  import androidx.core.view.WindowInsetsCompat
14
14
  import com.facebook.react.bridge.Arguments
15
15
  import com.facebook.react.bridge.WritableMap
16
- import com.facebook.react.modules.core.DeviceEventManagerModule
17
16
  import com.facebook.react.uimanager.ThemedReactContext
18
17
  import com.facebook.react.uimanager.UIManagerHelper
19
18
  import com.facebook.react.views.textinput.ReactEditText
20
19
  import com.facebook.react.views.view.ReactViewGroup
21
- import com.reactnativekeyboardcontroller.InteractiveKeyboardProvider
22
20
  import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
23
21
  import com.reactnativekeyboardcontroller.extensions.dispatchEvent
24
22
  import com.reactnativekeyboardcontroller.extensions.dp
23
+ import com.reactnativekeyboardcontroller.extensions.emitEvent
25
24
  import com.reactnativekeyboardcontroller.extensions.isKeyboardAnimation
25
+ import com.reactnativekeyboardcontroller.interactive.InteractiveKeyboardProvider
26
26
  import kotlin.math.abs
27
27
 
28
28
  private val TAG = KeyboardAnimationCallback::class.qualifiedName
@@ -80,8 +80,8 @@ class KeyboardAnimationCallback(
80
80
  viewTagFocused,
81
81
  ),
82
82
  )
83
- this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
84
- this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
83
+ context.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
84
+ context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
85
85
  }
86
86
  }
87
87
  }
@@ -158,7 +158,7 @@ class KeyboardAnimationCallback(
158
158
  }
159
159
 
160
160
  layoutObserver?.syncUpLayout()
161
- this.emitEvent(
161
+ context.emitEvent(
162
162
  "KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow",
163
163
  getEventParams(keyboardHeight),
164
164
  )
@@ -255,7 +255,7 @@ class KeyboardAnimationCallback(
255
255
  }
256
256
  isKeyboardVisible = isKeyboardVisible || isKeyboardShown
257
257
 
258
- this.emitEvent(
258
+ context.emitEvent(
259
259
  "KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow",
260
260
  getEventParams(keyboardHeight),
261
261
  )
@@ -299,7 +299,7 @@ class KeyboardAnimationCallback(
299
299
  this.animation?.cancel()
300
300
  }
301
301
 
302
- this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
302
+ context.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
303
303
  context.dispatchEvent(
304
304
  view.id,
305
305
  KeyboardTransitionEvent(
@@ -331,7 +331,7 @@ class KeyboardAnimationCallback(
331
331
  )
332
332
  }
333
333
  animation.doOnEnd {
334
- this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
334
+ context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
335
335
  context.dispatchEvent(
336
336
  view.id,
337
337
  KeyboardTransitionEvent(
@@ -368,12 +368,6 @@ class KeyboardAnimationCallback(
368
368
  return (keyboardHeight - navigationBar).toFloat().dp.coerceAtLeast(0.0)
369
369
  }
370
370
 
371
- private fun emitEvent(event: String, params: WritableMap) {
372
- context?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params)
373
-
374
- Log.i(TAG, event)
375
- }
376
-
377
371
  private fun getEventParams(height: Double): WritableMap {
378
372
  val params: WritableMap = Arguments.createMap()
379
373
  params.putDouble("height", height)
@@ -6,6 +6,8 @@ import android.view.WindowManager
6
6
  import android.view.inputmethod.InputMethodManager
7
7
  import com.facebook.react.bridge.ReactApplicationContext
8
8
  import com.facebook.react.bridge.UiThreadUtil
9
+ import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
10
+ import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator
9
11
 
10
12
  class KeyboardControllerModuleImpl(private val mReactContext: ReactApplicationContext) {
11
13
  private val mDefaultMode: Int = getCurrentMode()
@@ -28,6 +30,19 @@ class KeyboardControllerModuleImpl(private val mReactContext: ReactApplicationCo
28
30
  }
29
31
  }
30
32
 
33
+ fun setFocusTo(direction: String) {
34
+ if (direction == "current") {
35
+ return FocusedInputHolder.requestFocus()
36
+ }
37
+
38
+ val activity = mReactContext.currentActivity
39
+ val view: View? = activity?.currentFocus
40
+
41
+ if (view != null) {
42
+ ViewHierarchyNavigator.setFocusTo(direction, view)
43
+ }
44
+ }
45
+
31
46
  private fun setSoftInputMode(mode: Int) {
32
47
  UiThreadUtil.runOnUiThread {
33
48
  if (getCurrentMode() != mode) {
@@ -0,0 +1,16 @@
1
+ package com.reactnativekeyboardcontroller.traversal
2
+
3
+ import com.facebook.react.views.textinput.ReactEditText
4
+ import java.lang.ref.WeakReference
5
+
6
+ object FocusedInputHolder {
7
+ private var input: WeakReference<ReactEditText?>? = null
8
+
9
+ fun set(textInput: ReactEditText) {
10
+ input = WeakReference(textInput)
11
+ }
12
+
13
+ fun requestFocus() {
14
+ input?.get()?.requestFocusFromJS()
15
+ }
16
+ }
@@ -0,0 +1,101 @@
1
+ package com.reactnativekeyboardcontroller.traversal
2
+
3
+ import android.view.View
4
+ import android.view.ViewGroup
5
+ import android.widget.EditText
6
+ import com.facebook.react.bridge.UiThreadUtil
7
+ import com.facebook.react.views.textinput.ReactEditText
8
+
9
+ object ViewHierarchyNavigator {
10
+ fun setFocusTo(direction: String, view: View) {
11
+ val input = if (direction == "next") findNextEditText(view) else findPreviousEditText(view)
12
+
13
+ UiThreadUtil.runOnUiThread {
14
+ if (input is ReactEditText) {
15
+ input.requestFocusFromJS()
16
+ } else {
17
+ input?.requestFocus()
18
+ }
19
+ }
20
+ }
21
+
22
+ fun getAllInputFields(viewGroup: View?): List<EditText> {
23
+ val editTexts = mutableListOf<EditText>()
24
+
25
+ // Helper function to recursively search for EditText views
26
+ fun findEditTexts(view: View?) {
27
+ if (isValidTextInput(view)) {
28
+ editTexts.add(view as EditText)
29
+ } else if (view is ViewGroup) {
30
+ for (i in 0 until view.childCount) {
31
+ findEditTexts(view.getChildAt(i))
32
+ }
33
+ }
34
+ }
35
+
36
+ // Start the search with the provided viewGroup
37
+ findEditTexts(viewGroup)
38
+
39
+ return editTexts
40
+ }
41
+
42
+ private fun findNextEditText(currentFocus: View): EditText? {
43
+ return findEditTextInDirection(currentFocus, 1)
44
+ }
45
+
46
+ private fun findPreviousEditText(currentFocus: View): EditText? {
47
+ return findEditTextInDirection(currentFocus, -1)
48
+ }
49
+
50
+ @Suppress("detekt:ReturnCount")
51
+ private fun findEditTextInDirection(currentFocus: View, direction: Int): EditText? {
52
+ // Attempt to find the parent view group, return null if not found or not a ViewGroup
53
+ val parentViewGroup = currentFocus.parent as? ViewGroup ?: return null
54
+
55
+ // Find the index of the current EditText in its parent
56
+ val currentIndex = parentViewGroup.indexOfChild(currentFocus)
57
+
58
+ // Determine the start index and end condition for the loop based on the direction
59
+ var i = if (direction > 0) currentIndex + 1 else currentIndex - 1
60
+ val end = if (direction > 0) parentViewGroup.childCount else -1
61
+
62
+ // Iterate over siblings in the specified direction
63
+ while (i != end) {
64
+ val nextChild = parentViewGroup.getChildAt(i)
65
+ findEditTextOrGoDeeper(nextChild, direction)?.let { return it } // Return if an EditText is found
66
+ i += direction
67
+ }
68
+
69
+ // Recurse to the parent's parent if no sibling EditText is found
70
+ return findEditTextInDirection(parentViewGroup, direction)
71
+ }
72
+
73
+ private fun findEditTextInHierarchy(viewGroup: ViewGroup, direction: Int): EditText? {
74
+ val range = if (direction > 0) 0 until viewGroup.childCount else viewGroup.childCount - 1 downTo 0
75
+
76
+ for (i in range) {
77
+ val child = viewGroup.getChildAt(i)
78
+ findEditTextOrGoDeeper(child, direction)?.let { return it }
79
+ }
80
+
81
+ // No EditText found in the current view group
82
+ return null
83
+ }
84
+
85
+ private fun findEditTextOrGoDeeper(child: View, direction: Int): EditText? {
86
+ var result: EditText? = null
87
+
88
+ if (isValidTextInput(child)) {
89
+ result = child as EditText
90
+ } else if (child is ViewGroup) {
91
+ // If the child is a ViewGroup, check its children recursively
92
+ result = findEditTextInHierarchy(child, direction)
93
+ }
94
+
95
+ return result
96
+ }
97
+
98
+ private fun isValidTextInput(view: View?): Boolean {
99
+ return view is EditText && view.isEnabled
100
+ }
101
+ }
@@ -11,11 +11,11 @@ import androidx.core.view.ViewCompat
11
11
  import androidx.core.view.WindowInsetsCompat
12
12
  import com.facebook.react.uimanager.ThemedReactContext
13
13
  import com.facebook.react.views.view.ReactViewGroup
14
- import com.reactnativekeyboardcontroller.KeyboardAnimationController
15
14
  import com.reactnativekeyboardcontroller.extensions.copyBoundsInWindow
16
- import com.reactnativekeyboardcontroller.interpolators.Interpolator
17
- import com.reactnativekeyboardcontroller.interpolators.IosInterpolator
18
- import com.reactnativekeyboardcontroller.interpolators.LinearInterpolator
15
+ import com.reactnativekeyboardcontroller.interactive.KeyboardAnimationController
16
+ import com.reactnativekeyboardcontroller.interactive.interpolators.Interpolator
17
+ import com.reactnativekeyboardcontroller.interactive.interpolators.IosInterpolator
18
+ import com.reactnativekeyboardcontroller.interactive.interpolators.LinearInterpolator
19
19
  import kotlin.math.absoluteValue
20
20
  import kotlin.math.roundToInt
21
21
 
@@ -25,6 +25,11 @@ class KeyboardControllerModule(mReactContext: ReactApplicationContext) : ReactCo
25
25
  module.dismiss()
26
26
  }
27
27
 
28
+ @ReactMethod
29
+ fun setFocusTo(direction: String) {
30
+ module.setFocusTo(direction)
31
+ }
32
+
28
33
  @Suppress("detekt:UnusedParameter")
29
34
  @ReactMethod
30
35
  fun addListener(eventName: String?) {
@@ -0,0 +1,188 @@
1
+ package com.reactnativekeyboardcontroller.traversal
2
+
3
+ import android.content.Context
4
+ import android.widget.EditText
5
+ import android.widget.LinearLayout
6
+ import androidx.test.core.app.ApplicationProvider
7
+ import org.junit.Assert.assertTrue
8
+ import org.junit.Before
9
+ import org.junit.Test
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+ import org.robolectric.shadows.ShadowLooper
13
+
14
+ @RunWith(RobolectricTestRunner::class)
15
+ class ViewHierarchyNavigatorTest {
16
+ private lateinit var layout: LinearLayout
17
+ private lateinit var editText1: EditText
18
+ private lateinit var editText2: EditText
19
+ private lateinit var editText3: EditText
20
+ private lateinit var editText4: EditText
21
+ private lateinit var editText5: EditText
22
+ private lateinit var editText6: EditText
23
+ private lateinit var editText7: EditText
24
+ private lateinit var editText8: EditText
25
+ private lateinit var editText9: EditText
26
+ private lateinit var editText10: EditText
27
+ private lateinit var editText11: EditText
28
+ private lateinit var editText12: EditText
29
+ private lateinit var editText13: EditText
30
+
31
+ @Suppress("detekt:CyclomaticComplexMethod")
32
+ @Before
33
+ fun setUp() {
34
+ val context = ApplicationProvider.getApplicationContext<Context>()
35
+
36
+ editText1 = EditText(context).apply { id = 1 }
37
+ editText2 = EditText(context).apply { id = 2 }
38
+ editText3 = EditText(context).apply { id = 3; isEnabled = false }
39
+ editText4 = EditText(context).apply { id = 4; isEnabled = false }
40
+ editText5 = EditText(context).apply { id = 5 }
41
+ editText6 = EditText(context).apply { id = 6 }
42
+ editText7 = EditText(context).apply { id = 7 }
43
+ editText8 = EditText(context).apply { id = 8 }
44
+ editText9 = EditText(context).apply { id = 9 }
45
+ editText10 = EditText(context).apply { id = 10 }
46
+ editText11 = EditText(context).apply { id = 11 }
47
+ editText12 = EditText(context).apply { id = 12 }
48
+ editText13 = EditText(context).apply { id = 13 }
49
+
50
+ layout = LinearLayout(context).apply {
51
+ addView(editText1)
52
+ addView(editText2)
53
+ addView(editText3)
54
+ addView(editText4)
55
+ addView(
56
+ LinearLayout(context).apply {
57
+ addView(editText5)
58
+ addView(editText6)
59
+ addView(editText7)
60
+ },
61
+ )
62
+ addView(editText8)
63
+ addView(editText9)
64
+ addView(editText10)
65
+ addView(editText11)
66
+ addView(editText12)
67
+ addView(editText13)
68
+ }
69
+ }
70
+
71
+ @Test
72
+ fun `getAllInputFields returns all EditTexts in ViewGroup`() {
73
+ val editTexts = ViewHierarchyNavigator.getAllInputFields(layout)
74
+
75
+ // 13 (all) - 2 (disabled)
76
+ assertTrue(editTexts.size == 11)
77
+ }
78
+
79
+ @Test
80
+ fun `setFocusTo to 'next' should set focus to next field`() {
81
+ editText1.requestFocus()
82
+
83
+ ViewHierarchyNavigator.setFocusTo("next", editText1)
84
+
85
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
86
+
87
+ assertTrue(editText2.hasFocus())
88
+ }
89
+
90
+ @Test
91
+ fun `setFocusTo to 'prev' should set focus to previous field`() {
92
+ editText2.requestFocus()
93
+
94
+ ViewHierarchyNavigator.setFocusTo("prev", editText2)
95
+
96
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
97
+
98
+ assertTrue(editText1.hasFocus())
99
+ }
100
+
101
+ @Test
102
+ fun `setFocusTo to 'next' should skip non-editable fields`() {
103
+ editText2.requestFocus()
104
+
105
+ ViewHierarchyNavigator.setFocusTo("next", editText2)
106
+
107
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
108
+
109
+ assertTrue(editText5.hasFocus())
110
+ }
111
+
112
+ @Test
113
+ fun `setFocusTo to 'prev' should skip non-editable fields`() {
114
+ editText5.requestFocus()
115
+
116
+ ViewHierarchyNavigator.setFocusTo("prev", editText5)
117
+
118
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
119
+
120
+ assertTrue(editText2.hasFocus())
121
+ }
122
+
123
+ @Test
124
+ fun `setFocusTo to 'next' should set focus relatively to current group`() {
125
+ editText5.requestFocus()
126
+
127
+ ViewHierarchyNavigator.setFocusTo("next", editText5)
128
+
129
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
130
+
131
+ assertTrue(editText6.hasFocus())
132
+ }
133
+
134
+ @Test
135
+ fun `setFocusTo to 'prev' should set focus relatively to current group`() {
136
+ editText7.requestFocus()
137
+
138
+ ViewHierarchyNavigator.setFocusTo("prev", editText7)
139
+
140
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
141
+
142
+ assertTrue(editText6.hasFocus())
143
+ }
144
+
145
+ @Test
146
+ fun `setFocusTo to 'next' should correctly exit from current group`() {
147
+ editText7.requestFocus()
148
+
149
+ ViewHierarchyNavigator.setFocusTo("next", editText7)
150
+
151
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
152
+
153
+ assertTrue(editText8.hasFocus())
154
+ }
155
+
156
+ @Test
157
+ fun `setFocusTo to 'prev' should set focus to last element in group`() {
158
+ editText8.requestFocus()
159
+
160
+ ViewHierarchyNavigator.setFocusTo("prev", editText8)
161
+
162
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
163
+
164
+ assertTrue(editText7.hasFocus())
165
+ }
166
+
167
+ @Test
168
+ fun `setFocusTo to 'next' should do nothing if it's last element`() {
169
+ editText13.requestFocus()
170
+
171
+ ViewHierarchyNavigator.setFocusTo("next", editText13)
172
+
173
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
174
+
175
+ assertTrue(editText13.hasFocus())
176
+ }
177
+
178
+ @Test
179
+ fun `setFocusTo to 'prev' should do nothing if it's first element`() {
180
+ editText1.requestFocus()
181
+
182
+ ViewHierarchyNavigator.setFocusTo("prev", editText1)
183
+
184
+ ShadowLooper.runUiThreadTasksIncludingDelayedTasks()
185
+
186
+ assertTrue(editText1.hasFocus())
187
+ }
188
+ }
@@ -6,5 +6,9 @@ line_length:
6
6
  warning: 120
7
7
  ignores_urls: true
8
8
 
9
+ identifier_name:
10
+ excluded:
11
+ - i # allow `i` variable in cycles
12
+
9
13
  excluded:
10
14
  - Pods