react-native-keyboard-controller 1.10.6 → 1.11.1
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.
- package/README.md +1 -0
- package/android/build.gradle +6 -0
- package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +4 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt +2 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +22 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +9 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +20 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +7 -13
- package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +15 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/FocusedInputHolder.kt +16 -0
- package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt +101 -0
- package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +5 -0
- package/android/src/test/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigatorTest.kt +188 -0
- package/ios/.swiftlint.yml +4 -0
- package/ios/Extensions.swift +29 -0
- package/ios/KeyboardController.xcodeproj/project.pbxproj +20 -0
- package/ios/KeyboardControllerModule.mm +18 -0
- package/ios/events/FocusedInputLayoutChangedEvent.m +3 -0
- package/ios/observers/FocusedInputObserver.swift +20 -4
- package/ios/observers/KeyboardMovementObserver.swift +5 -4
- package/ios/traversal/FocusedInputHolder.swift +32 -0
- package/ios/traversal/TextInput.swift +26 -0
- package/ios/traversal/ViewHierarchyNavigator.swift +116 -0
- package/ios/views/KeyboardControllerView.mm +5 -0
- package/ios/views/KeyboardControllerViewManager.swift +4 -1
- package/jest/index.js +20 -0
- package/lib/commonjs/bindings.js +12 -1
- package/lib/commonjs/bindings.js.map +1 -1
- package/lib/commonjs/bindings.native.js +11 -2
- package/lib/commonjs/bindings.native.js.map +1 -1
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js +18 -6
- package/lib/commonjs/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/commonjs/components/KeyboardToolbar/Arrow.js +79 -0
- package/lib/commonjs/components/KeyboardToolbar/Arrow.js.map +1 -0
- package/lib/commonjs/components/KeyboardToolbar/Button.js +74 -0
- package/lib/commonjs/components/KeyboardToolbar/Button.js.map +1 -0
- package/lib/commonjs/components/KeyboardToolbar/colors.js +22 -0
- package/lib/commonjs/components/KeyboardToolbar/colors.js.map +1 -0
- package/lib/commonjs/components/KeyboardToolbar/colors.native.js +41 -0
- package/lib/commonjs/components/KeyboardToolbar/colors.native.js.map +1 -0
- package/lib/commonjs/components/KeyboardToolbar/index.js +136 -0
- package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -0
- package/lib/commonjs/components/KeyboardToolbar/types.js +6 -0
- package/lib/commonjs/components/KeyboardToolbar/types.js.map +1 -0
- package/lib/commonjs/components/hooks/useColorScheme.js +11 -0
- package/lib/commonjs/components/hooks/useColorScheme.js.map +1 -0
- package/lib/commonjs/components/hooks/useKeyboardInterpolation.js +3 -2
- package/lib/commonjs/components/hooks/useKeyboardInterpolation.js.map +1 -1
- package/lib/commonjs/components/index.js +21 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/index.js +22 -13
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/module/bindings.js +10 -0
- package/lib/module/bindings.js.map +1 -1
- package/lib/module/bindings.native.js +9 -1
- package/lib/module/bindings.native.js.map +1 -1
- package/lib/module/components/KeyboardAwareScrollView/index.js +18 -6
- package/lib/module/components/KeyboardAwareScrollView/index.js.map +1 -1
- package/lib/module/components/KeyboardToolbar/Arrow.js +69 -0
- package/lib/module/components/KeyboardToolbar/Arrow.js.map +1 -0
- package/lib/module/components/KeyboardToolbar/Button.js +64 -0
- package/lib/module/components/KeyboardToolbar/Button.js.map +1 -0
- package/lib/module/components/KeyboardToolbar/colors.js +15 -0
- package/lib/module/components/KeyboardToolbar/colors.js.map +1 -0
- package/lib/module/components/KeyboardToolbar/colors.native.js +34 -0
- package/lib/module/components/KeyboardToolbar/colors.native.js.map +1 -0
- package/lib/module/components/KeyboardToolbar/index.js +121 -0
- package/lib/module/components/KeyboardToolbar/index.js.map +1 -0
- package/lib/module/components/KeyboardToolbar/types.js +2 -0
- package/lib/module/components/KeyboardToolbar/types.js.map +1 -0
- package/lib/module/components/hooks/useColorScheme.js +4 -0
- package/lib/module/components/hooks/useColorScheme.js.map +1 -0
- package/lib/module/components/hooks/useKeyboardInterpolation.js +3 -2
- package/lib/module/components/hooks/useKeyboardInterpolation.js.map +1 -1
- package/lib/module/components/index.js +1 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/index.js +3 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
- package/lib/module/specs/NativeKeyboardController.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/bindings.d.ts +6 -1
- package/lib/typescript/bindings.native.d.ts +6 -1
- package/lib/typescript/components/KeyboardToolbar/Arrow.d.ts +9 -0
- package/lib/typescript/components/KeyboardToolbar/Button.d.ts +15 -0
- package/lib/typescript/components/KeyboardToolbar/colors.d.ts +2 -0
- package/lib/typescript/components/KeyboardToolbar/colors.native.d.ts +2 -0
- package/lib/typescript/components/KeyboardToolbar/index.d.ts +25 -0
- package/lib/typescript/components/KeyboardToolbar/types.d.ts +16 -0
- package/lib/typescript/components/hooks/useColorScheme.d.ts +2 -0
- package/lib/typescript/components/index.d.ts +1 -0
- package/lib/typescript/index.d.ts +1 -2
- package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +1 -0
- package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -0
- package/lib/typescript/types.d.ts +11 -0
- package/package.json +5 -1
- package/src/bindings.native.ts +11 -1
- package/src/bindings.ts +9 -0
- package/src/components/KeyboardAwareScrollView/index.tsx +22 -2
- package/src/components/KeyboardToolbar/Arrow.tsx +80 -0
- package/src/components/KeyboardToolbar/Button.tsx +98 -0
- package/src/components/KeyboardToolbar/colors.native.ts +37 -0
- package/src/components/KeyboardToolbar/colors.ts +16 -0
- package/src/components/KeyboardToolbar/index.tsx +154 -0
- package/src/components/KeyboardToolbar/types.ts +16 -0
- package/src/components/hooks/useColorScheme.ts +5 -0
- package/src/components/hooks/useKeyboardInterpolation.ts +3 -2
- package/src/components/index.ts +5 -0
- package/src/index.ts +4 -1
- package/src/specs/KeyboardControllerViewNativeComponent.ts +1 -0
- package/src/specs/NativeKeyboardController.ts +1 -0
- package/src/types.ts +14 -0
- package/lib/commonjs/replicas.js +0 -125
- package/lib/commonjs/replicas.js.map +0 -1
- package/lib/module/replicas.js +0 -115
- package/lib/module/replicas.js.map +0 -1
- package/lib/typescript/replicas.d.ts +0 -38
- 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! 😊
|
package/android/build.gradle
CHANGED
|
@@ -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
|
}
|
package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt
CHANGED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt
CHANGED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt
CHANGED
|
@@ -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,7 +13,6 @@ 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
|
|
@@ -21,6 +20,7 @@ import com.facebook.react.views.view.ReactViewGroup
|
|
|
21
20
|
import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
|
|
22
21
|
import com.reactnativekeyboardcontroller.extensions.dispatchEvent
|
|
23
22
|
import com.reactnativekeyboardcontroller.extensions.dp
|
|
23
|
+
import com.reactnativekeyboardcontroller.extensions.emitEvent
|
|
24
24
|
import com.reactnativekeyboardcontroller.extensions.isKeyboardAnimation
|
|
25
25
|
import com.reactnativekeyboardcontroller.interactive.InteractiveKeyboardProvider
|
|
26
26
|
import kotlin.math.abs
|
|
@@ -80,8 +80,8 @@ class KeyboardAnimationCallback(
|
|
|
80
80
|
viewTagFocused,
|
|
81
81
|
),
|
|
82
82
|
)
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/FocusedInputHolder.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/main/java/com/reactnativekeyboardcontroller/traversal/ViewHierarchyNavigator.kt
ADDED
|
@@ -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
|
+
}
|
package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt
CHANGED
|
@@ -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
|
+
}
|
package/ios/.swiftlint.yml
CHANGED
package/ios/Extensions.swift
CHANGED
|
@@ -53,6 +53,35 @@ public extension Optional where Wrapped == UIResponder {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
public extension UIScrollView {
|
|
57
|
+
var reactViewTag: NSNumber {
|
|
58
|
+
#if KEYBOARD_CONTROLLER_NEW_ARCH_ENABLED
|
|
59
|
+
return (superview?.tag ?? -1) as NSNumber
|
|
60
|
+
#else
|
|
61
|
+
return superview?.reactTag ?? -1
|
|
62
|
+
#endif
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public extension Optional where Wrapped: UIResponder {
|
|
67
|
+
var parentScrollViewTarget: NSNumber {
|
|
68
|
+
var currentResponder: UIResponder? = self
|
|
69
|
+
|
|
70
|
+
while let currentView = currentResponder {
|
|
71
|
+
// If the current responder is a UIScrollView (excluding UITextView), return its tag
|
|
72
|
+
if let scrollView = currentView as? UIScrollView, !(currentView is UITextView) {
|
|
73
|
+
return scrollView.reactViewTag
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Move to the next responder in the chain
|
|
77
|
+
currentResponder = currentView.next
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// UIScrollView is not found
|
|
81
|
+
return -1
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
56
85
|
public extension UIView {
|
|
57
86
|
var globalFrame: CGRect? {
|
|
58
87
|
let rootView = UIApplication.shared.keyWindow?.rootViewController?.view
|