react-native-keyboard-controller 1.14.4 → 1.15.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 (95) hide show
  1. package/android/build.gradle +3 -2
  2. package/android/gradle.properties +1 -0
  3. package/android/react-native-helpers.gradle +5 -11
  4. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +2 -2
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +35 -0
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +18 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +5 -0
  8. package/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt +9 -6
  9. package/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +13 -4
  10. package/android/src/main/java/com/reactnativekeyboardcontroller/views/overlay/JSPointerDispatcherCompat.kt +56 -0
  11. package/android/src/main/java/com/reactnativekeyboardcontroller/views/overlay/OverKeyboardViewGroup.kt +10 -7
  12. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt +2 -2
  13. package/ios/KeyboardControllerModule.mm +3 -3
  14. package/ios/KeyboardControllerModuleImpl.swift +66 -0
  15. package/ios/events/KeyboardEventEmitterPayload.swift +23 -0
  16. package/ios/extensions/Notification.swift +17 -0
  17. package/ios/extensions/UIKeyboardAppearance.swift +21 -0
  18. package/ios/extensions/UIKeyboardType.swift +30 -0
  19. package/ios/observers/KeyboardMovementObserver.swift +8 -27
  20. package/ios/{traversal → protocols}/TextInput.swift +5 -1
  21. package/ios/traversal/ViewHierarchyNavigator.swift +4 -1
  22. package/jest/index.js +20 -15
  23. package/lib/commonjs/animated.js +6 -0
  24. package/lib/commonjs/animated.js.map +1 -1
  25. package/lib/commonjs/bindings.js +2 -2
  26. package/lib/commonjs/bindings.js.map +1 -1
  27. package/lib/commonjs/bindings.native.js +4 -3
  28. package/lib/commonjs/bindings.native.js.map +1 -1
  29. package/lib/commonjs/components/KeyboardAvoidingView/index.js.map +1 -1
  30. package/lib/commonjs/components/KeyboardStickyView/index.js +3 -2
  31. package/lib/commonjs/components/KeyboardStickyView/index.js.map +1 -1
  32. package/lib/commonjs/components/KeyboardToolbar/index.js +14 -9
  33. package/lib/commonjs/components/KeyboardToolbar/index.js.map +1 -1
  34. package/lib/commonjs/hooks/index.js +3 -3
  35. package/lib/commonjs/hooks/index.js.map +1 -1
  36. package/lib/commonjs/hooks/useWindowDimensions/index.android.js +5 -0
  37. package/lib/commonjs/hooks/useWindowDimensions/index.android.js.map +1 -1
  38. package/lib/commonjs/index.js +12 -0
  39. package/lib/commonjs/index.js.map +1 -1
  40. package/lib/commonjs/internal.js +2 -2
  41. package/lib/commonjs/internal.js.map +1 -1
  42. package/lib/commonjs/module.js +45 -0
  43. package/lib/commonjs/module.js.map +1 -0
  44. package/lib/commonjs/specs/NativeKeyboardController.js.map +1 -1
  45. package/lib/commonjs/types.js.map +1 -1
  46. package/lib/module/animated.js +6 -0
  47. package/lib/module/animated.js.map +1 -1
  48. package/lib/module/bindings.js +1 -1
  49. package/lib/module/bindings.js.map +1 -1
  50. package/lib/module/bindings.native.js +3 -2
  51. package/lib/module/bindings.native.js.map +1 -1
  52. package/lib/module/components/KeyboardAvoidingView/index.js.map +1 -1
  53. package/lib/module/components/KeyboardStickyView/index.js +3 -2
  54. package/lib/module/components/KeyboardStickyView/index.js.map +1 -1
  55. package/lib/module/components/KeyboardToolbar/index.js +15 -10
  56. package/lib/module/components/KeyboardToolbar/index.js.map +1 -1
  57. package/lib/module/hooks/index.js +1 -1
  58. package/lib/module/hooks/index.js.map +1 -1
  59. package/lib/module/hooks/useWindowDimensions/index.android.js +5 -0
  60. package/lib/module/hooks/useWindowDimensions/index.android.js.map +1 -1
  61. package/lib/module/index.js +1 -0
  62. package/lib/module/index.js.map +1 -1
  63. package/lib/module/internal.js +2 -2
  64. package/lib/module/internal.js.map +1 -1
  65. package/lib/module/module.js +39 -0
  66. package/lib/module/module.js.map +1 -0
  67. package/lib/module/specs/NativeKeyboardController.js.map +1 -1
  68. package/lib/module/types.js.map +1 -1
  69. package/lib/typescript/bindings.d.ts +5 -6
  70. package/lib/typescript/bindings.native.d.ts +2 -3
  71. package/lib/typescript/components/KeyboardAvoidingView/index.d.ts +14 -24
  72. package/lib/typescript/components/KeyboardAwareScrollView/index.d.ts +6 -6
  73. package/lib/typescript/components/KeyboardAwareScrollView/utils.d.ts +1 -1
  74. package/lib/typescript/components/KeyboardStickyView/index.d.ts +8 -4
  75. package/lib/typescript/components/KeyboardToolbar/Button.d.ts +2 -1
  76. package/lib/typescript/components/KeyboardToolbar/index.d.ts +2 -1
  77. package/lib/typescript/index.d.ts +1 -0
  78. package/lib/typescript/internal.d.ts +0 -1
  79. package/lib/typescript/module.d.ts +2 -0
  80. package/lib/typescript/specs/NativeKeyboardController.d.ts +1 -1
  81. package/lib/typescript/types.d.ts +15 -2
  82. package/package.json +6 -4
  83. package/src/animated.tsx +6 -0
  84. package/src/bindings.native.ts +5 -4
  85. package/src/bindings.ts +2 -2
  86. package/src/components/KeyboardAvoidingView/index.tsx +27 -11
  87. package/src/components/KeyboardStickyView/index.tsx +12 -3
  88. package/src/components/KeyboardToolbar/index.tsx +14 -11
  89. package/src/hooks/index.ts +1 -1
  90. package/src/hooks/useWindowDimensions/index.android.ts +5 -0
  91. package/src/index.ts +1 -0
  92. package/src/internal.ts +2 -2
  93. package/src/module.ts +50 -0
  94. package/src/specs/NativeKeyboardController.ts +1 -1
  95. package/src/types.ts +17 -1
@@ -158,14 +158,15 @@ repositories {
158
158
  }
159
159
 
160
160
  def kotlin_version = getExtOrDefault('kotlinVersion')
161
+ def reactNativeVersion = getExtOrDefault('reactNativeVersion')
161
162
 
162
163
  dependencies {
163
164
  if (project.ext.shouldConsumeReactNativeFromMavenCentral()) {
164
165
  // noinspection GradleDynamicVersion
165
- implementation 'com.facebook.react:react-android:+'
166
+ implementation "com.facebook.react:react-android:$reactNativeVersion"
166
167
  } else {
167
168
  // noinspection GradleDynamicVersion
168
- implementation 'com.facebook.react:react-native:+'
169
+ implementation "com.facebook.react:react-native:$reactNativeVersion"
169
170
  }
170
171
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
171
172
  implementation 'androidx.core:core-ktx:1.5.0-beta03'
@@ -2,5 +2,6 @@ KeyboardController_kotlinVersion=1.8.22
2
2
  KeyboardController_compileSdkVersion=33
3
3
  KeyboardController_targetSdkVersion=33
4
4
  KeyboardController_minSdkVersion=16
5
+ KeyboardController_reactNativeVersion=+
5
6
 
6
7
  android.useAndroidX=true
@@ -4,23 +4,17 @@ def safeAppExtGet(prop, fallback) {
4
4
  }
5
5
 
6
6
  // Let's detect react-native's directory, it will be used to determine RN's version
7
- // https://github.com/software-mansion/react-native-reanimated/blob/cda4627c3337c33674f05f755b7485165c6caca9/android/build.gradle#L88
7
+ // https://github.com/software-mansion/react-native-reanimated/blob/36c291a15880c78a94dd125c51484630546ceb7c/packages/react-native-reanimated/android/build.gradle#L73
8
8
  def resolveReactNativeDirectory() {
9
9
  def reactNativeLocation = safeAppExtGet("REACT_NATIVE_NODE_MODULES_DIR", null)
10
10
  if (reactNativeLocation != null) {
11
11
  return file(reactNativeLocation)
12
12
  }
13
13
 
14
- // monorepo workaround
15
- // react-native can be hoisted or in project's own node_modules
16
- def reactNativeFromProjectNodeModules = file("${rootProject.projectDir}/../node_modules/react-native")
17
- if (reactNativeFromProjectNodeModules.exists()) {
18
- return reactNativeFromProjectNodeModules
19
- }
20
-
21
- def reactNativeFromNodeModules = file("${projectDir}/../../react-native")
22
- if (reactNativeFromNodeModules.exists()) {
23
- return reactNativeFromNodeModules
14
+ // Fallback to node resolver for custom directory structures like monorepos.
15
+ def reactNativePackage = file(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim())
16
+ if (reactNativePackage.exists()) {
17
+ return reactNativePackage.parentFile
24
18
  }
25
19
 
26
20
  throw new GradleException(
@@ -18,8 +18,8 @@ class KeyboardControllerModule(
18
18
  module.setDefaultMode()
19
19
  }
20
20
 
21
- override fun dismiss() {
22
- module.dismiss()
21
+ override fun dismiss(keepFocus: Boolean) {
22
+ module.dismiss(keepFocus)
23
23
  }
24
24
 
25
25
  override fun setFocusTo(direction: String) {
@@ -2,6 +2,7 @@ package com.reactnativekeyboardcontroller.extensions
2
2
 
3
3
  import android.os.Build
4
4
  import android.text.Editable
5
+ import android.text.InputType
5
6
  import android.text.TextWatcher
6
7
  import android.view.View
7
8
  import android.widget.EditText
@@ -110,6 +111,40 @@ fun EditText?.focus() {
110
111
  }
111
112
  }
112
113
 
114
+ val EditText?.keyboardType: String
115
+ get() {
116
+ if (this == null) {
117
+ return "default"
118
+ }
119
+
120
+ // Extract base input type class
121
+ val inputTypeClass = inputType and InputType.TYPE_MASK_CLASS
122
+ val inputTypeVariation = inputType and InputType.TYPE_MASK_VARIATION
123
+
124
+ // Check for special input types
125
+ return when {
126
+ inputTypeVariation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS -> "email-address"
127
+ inputTypeVariation == InputType.TYPE_TEXT_VARIATION_URI -> "url"
128
+ inputTypeVariation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD -> "visible-password"
129
+
130
+ // Check for specific input type classes
131
+ inputTypeClass == InputType.TYPE_CLASS_NUMBER ->
132
+ when {
133
+ (inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL) != 0 &&
134
+ (inputType and InputType.TYPE_NUMBER_FLAG_SIGNED) == 0 -> "decimal-pad"
135
+
136
+ (inputType and InputType.TYPE_NUMBER_FLAG_SIGNED) != 0 -> "numeric"
137
+
138
+ else -> "number-pad"
139
+ }
140
+
141
+ inputTypeClass == InputType.TYPE_CLASS_PHONE -> "phone-pad"
142
+ inputTypeClass == InputType.TYPE_CLASS_TEXT -> "default"
143
+
144
+ else -> "default"
145
+ }
146
+ }
147
+
113
148
  class KeyboardControllerSelectionWatcher(
114
149
  private val editText: ReactEditText,
115
150
  private val action: (start: Int, end: Int, startX: Double, startY: Double, endX: Double, endY: Double) -> Unit,
@@ -1,5 +1,7 @@
1
1
  package com.reactnativekeyboardcontroller.extensions
2
2
 
3
+ import android.content.Context
4
+ import android.os.Build
3
5
  import com.facebook.react.bridge.ReactContext
4
6
  import com.facebook.react.bridge.WritableMap
5
7
  import com.facebook.react.modules.core.DeviceEventManagerModule
@@ -34,3 +36,19 @@ fun ThemedReactContext?.emitEvent(
34
36
 
35
37
  Logger.i("ThemedReactContext", event)
36
38
  }
39
+
40
+ val ThemedReactContext?.appearance: String
41
+ get() =
42
+ when {
43
+ this == null -> "default"
44
+ isSystemDarkMode(this) -> "dark"
45
+ else -> "light"
46
+ }
47
+
48
+ private fun isSystemDarkMode(context: Context): Boolean =
49
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
50
+ (context.getSystemService(Context.UI_MODE_SERVICE) as? android.app.UiModeManager)
51
+ ?.nightMode == android.app.UiModeManager.MODE_NIGHT_YES
52
+ } else {
53
+ false
54
+ }
@@ -15,12 +15,15 @@ import com.facebook.react.views.textinput.ReactEditText
15
15
  import com.facebook.react.views.view.ReactViewGroup
16
16
  import com.reactnativekeyboardcontroller.constants.Keyboard
17
17
  import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
18
+ import com.reactnativekeyboardcontroller.extensions.appearance
18
19
  import com.reactnativekeyboardcontroller.extensions.dispatchEvent
19
20
  import com.reactnativekeyboardcontroller.extensions.dp
20
21
  import com.reactnativekeyboardcontroller.extensions.emitEvent
21
22
  import com.reactnativekeyboardcontroller.extensions.isKeyboardAnimation
23
+ import com.reactnativekeyboardcontroller.extensions.keyboardType
22
24
  import com.reactnativekeyboardcontroller.interactive.InteractiveKeyboardProvider
23
25
  import com.reactnativekeyboardcontroller.log.Logger
26
+ import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
24
27
  import kotlin.math.abs
25
28
 
26
29
  private val TAG = KeyboardAnimationCallback::class.qualifiedName
@@ -425,6 +428,8 @@ class KeyboardAnimationCallback(
425
428
  params.putInt("duration", duration)
426
429
  params.putDouble("timestamp", System.currentTimeMillis().toDouble())
427
430
  params.putInt("target", viewTagFocused)
431
+ params.putString("type", FocusedInputHolder.get()?.keyboardType)
432
+ params.putString("appearance", context.appearance)
428
433
 
429
434
  return params
430
435
  }
@@ -65,14 +65,17 @@ class ModalAttachedWatcher(
65
65
  )
66
66
 
67
67
  rootView.addView(eventView)
68
- // on Android < 12 all events for `WindowInsetsAnimationCallback`
69
- // go through main `rootView`, so we don't need to stop main
70
- // callback - otherwise keyboard transitions will not be animated
71
- this.callback()?.suspend(areEventsComingFromOwnWindow)
72
- ViewCompat.setWindowInsetsAnimationCallback(rootView, callback)
73
- ViewCompat.setOnApplyWindowInsetsListener(eventView, callback)
74
68
 
75
69
  if (areEventsComingFromOwnWindow) {
70
+ // on Android < 12 all events for `WindowInsetsAnimationCallback`
71
+ // go through main `rootView`, so we don't need to stop main
72
+ // callback - otherwise keyboard transitions will not be animated
73
+ this.callback()?.suspend(true)
74
+ // attaching callback to Modal on Android < 12 can cause ghost animations, see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/718
75
+ // and overall attaching additional callbacks (if animation events go through the main window) is not necessary
76
+ ViewCompat.setWindowInsetsAnimationCallback(rootView, callback)
77
+ ViewCompat.setOnApplyWindowInsetsListener(eventView, callback)
78
+
76
79
  // when modal is shown then keyboard will be hidden by default
77
80
  //
78
81
  // - if events are coming from main window - then keyboard position
@@ -22,19 +22,28 @@ class KeyboardControllerModuleImpl(
22
22
  setSoftInputMode(mDefaultMode)
23
23
  }
24
24
 
25
- fun dismiss() {
25
+ fun dismiss(keepFocus: Boolean) {
26
26
  val activity = mReactContext.currentActivity
27
27
  val view: View? = FocusedInputHolder.get()
28
28
 
29
29
  if (view != null) {
30
- val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
31
- imm?.hideSoftInputFromWindow(view.windowToken, 0)
30
+ UiThreadUtil.runOnUiThread {
31
+ val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
32
+ imm?.hideSoftInputFromWindow(view.windowToken, 0)
33
+ if (!keepFocus) {
34
+ view.clearFocus()
35
+ }
36
+ }
32
37
  }
33
38
  }
34
39
 
35
40
  fun setFocusTo(direction: String) {
36
41
  if (direction == "current") {
37
- return FocusedInputHolder.focus()
42
+ UiThreadUtil.runOnUiThread {
43
+ FocusedInputHolder.focus()
44
+ }
45
+
46
+ return
38
47
  }
39
48
 
40
49
  val view: View? = FocusedInputHolder.get()
@@ -0,0 +1,56 @@
1
+ package com.reactnativekeyboardcontroller.views.overlay
2
+
3
+ import android.view.MotionEvent
4
+ import android.view.ViewGroup
5
+ import com.facebook.react.uimanager.JSPointerDispatcher
6
+ import com.facebook.react.uimanager.events.EventDispatcher
7
+ import java.lang.reflect.Method
8
+
9
+ /**
10
+ * Compat layer for `JSPointerDispatcher` interface for RN < 0.72
11
+ */
12
+ class JSPointerDispatcherCompat(
13
+ viewGroup: ViewGroup,
14
+ ) : JSPointerDispatcher(viewGroup) {
15
+ private val handleMotionEventMethod: Method? by lazy {
16
+ try {
17
+ // Try to get the 3-parameter method (for RN >= 0.72)
18
+ JSPointerDispatcher::class.java.getMethod(
19
+ HANDLE_MOTION_EVENT,
20
+ MotionEvent::class.java,
21
+ EventDispatcher::class.java,
22
+ Boolean::class.javaPrimitiveType,
23
+ )
24
+ } catch (_: NoSuchMethodException) {
25
+ try {
26
+ // Fallback to 2-parameter method (for RN < 0.72)
27
+ JSPointerDispatcher::class.java.getMethod(
28
+ HANDLE_MOTION_EVENT,
29
+ MotionEvent::class.java,
30
+ EventDispatcher::class.java,
31
+ )
32
+ } catch (_: NoSuchMethodException) {
33
+ null
34
+ }
35
+ }
36
+ }
37
+
38
+ fun handleMotionEventCompat(
39
+ event: MotionEvent?,
40
+ eventDispatcher: EventDispatcher?,
41
+ isCapture: Boolean,
42
+ ) {
43
+ handleMotionEventMethod?.let { method ->
44
+ if (method.parameterCount == RN_72_PARAMS_COUNT) {
45
+ method.invoke(this, event, eventDispatcher, isCapture)
46
+ } else {
47
+ method.invoke(this, event, eventDispatcher)
48
+ }
49
+ }
50
+ }
51
+
52
+ companion object {
53
+ private const val HANDLE_MOTION_EVENT = "handleMotionEvent"
54
+ private const val RN_72_PARAMS_COUNT = 3
55
+ }
56
+ }
@@ -8,7 +8,6 @@ import android.view.View
8
8
  import android.view.WindowManager
9
9
  import com.facebook.react.bridge.UiThreadUtil
10
10
  import com.facebook.react.config.ReactFeatureFlags
11
- import com.facebook.react.uimanager.JSPointerDispatcher
12
11
  import com.facebook.react.uimanager.JSTouchDispatcher
13
12
  import com.facebook.react.uimanager.ThemedReactContext
14
13
  import com.facebook.react.uimanager.UIManagerHelper
@@ -99,13 +98,13 @@ class OverKeyboardRootViewGroup(
99
98
  ) : ReactViewGroup(reactContext),
100
99
  RootViewCompat {
101
100
  private val jsTouchDispatcher: JSTouchDispatcher = JSTouchDispatcher(this)
102
- private var jsPointerDispatcher: JSPointerDispatcher? = null
101
+ private var jsPointerDispatcher: JSPointerDispatcherCompat? = null
103
102
  internal var eventDispatcher: EventDispatcher? = null
104
103
  internal var isAttached = false
105
104
 
106
105
  init {
107
106
  if (ReactFeatureFlags.dispatchPointerEvents) {
108
- jsPointerDispatcher = JSPointerDispatcher(this)
107
+ jsPointerDispatcher = JSPointerDispatcherCompat(this)
109
108
  }
110
109
  }
111
110
 
@@ -125,7 +124,7 @@ class OverKeyboardRootViewGroup(
125
124
  override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
126
125
  eventDispatcher?.let { eventDispatcher ->
127
126
  jsTouchDispatcher.handleTouchEvent(event, eventDispatcher)
128
- jsPointerDispatcher?.handleMotionEvent(event, eventDispatcher, true)
127
+ jsPointerDispatcher?.handleMotionEventCompat(event, eventDispatcher, true)
129
128
  }
130
129
  return super.onInterceptTouchEvent(event)
131
130
  }
@@ -134,7 +133,7 @@ class OverKeyboardRootViewGroup(
134
133
  override fun onTouchEvent(event: MotionEvent): Boolean {
135
134
  eventDispatcher?.let { eventDispatcher ->
136
135
  jsTouchDispatcher.handleTouchEvent(event, eventDispatcher)
137
- jsPointerDispatcher?.handleMotionEvent(event, eventDispatcher, false)
136
+ jsPointerDispatcher?.handleMotionEventCompat(event, eventDispatcher, false)
138
137
  }
139
138
  super.onTouchEvent(event)
140
139
  // In case when there is no children interested in handling touch event, we return true from
@@ -143,12 +142,16 @@ class OverKeyboardRootViewGroup(
143
142
  }
144
143
 
145
144
  override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
146
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, true) }
145
+ eventDispatcher?.let {
146
+ jsPointerDispatcher?.handleMotionEventCompat(event, it, true)
147
+ }
147
148
  return super.onHoverEvent(event)
148
149
  }
149
150
 
150
151
  override fun onHoverEvent(event: MotionEvent): Boolean {
151
- eventDispatcher?.let { jsPointerDispatcher?.handleMotionEvent(event, it, false) }
152
+ eventDispatcher?.let {
153
+ jsPointerDispatcher?.handleMotionEventCompat(event, it, false)
154
+ }
152
155
  return super.onHoverEvent(event)
153
156
  }
154
157
 
@@ -23,8 +23,8 @@ class KeyboardControllerModule(
23
23
  }
24
24
 
25
25
  @ReactMethod
26
- fun dismiss() {
27
- module.dismiss()
26
+ fun dismiss(keepFocus: Boolean) {
27
+ module.dismiss(keepFocus)
28
28
  }
29
29
 
30
30
  @ReactMethod
@@ -68,13 +68,13 @@ RCT_EXPORT_METHOD(setInputMode : (nonnull NSNumber *)mode)
68
68
  }
69
69
 
70
70
  #ifdef RCT_NEW_ARCH_ENABLED
71
- - (void)dismiss
71
+ - (void)dismiss:(BOOL)keepFocus
72
72
  #else
73
- RCT_EXPORT_METHOD(dismiss)
73
+ RCT_EXPORT_METHOD(dismiss : (BOOL)keepFocus)
74
74
  #endif
75
75
  {
76
76
  dispatch_async(dispatch_get_main_queue(), ^{
77
- [[UIResponder current] resignFirstResponder];
77
+ [KeyboardControllerModuleImpl dismiss:keepFocus];
78
78
  });
79
79
  }
80
80
 
@@ -0,0 +1,66 @@
1
+ //
2
+ // KeyboardControllerModuleImpl.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 19/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ @objc(KeyboardControllerModuleImpl)
12
+ public class KeyboardControllerModuleImpl: NSObject {
13
+ private static let keyboardRevealGestureName = "keyboardRevealGesture"
14
+
15
+ @objc
16
+ public static func dismiss(_ keepFocus: Bool) {
17
+ guard let input = UIResponder.current as? TextInput else { return }
18
+
19
+ if keepFocus {
20
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTextInputTapped(_:)))
21
+ tapGesture.name = keyboardRevealGestureName
22
+ input.addGestureRecognizer(tapGesture)
23
+
24
+ input.inputView = UIView()
25
+ input.reloadInputViews()
26
+
27
+ NotificationCenter.default.addObserver(
28
+ self,
29
+ selector: #selector(onResponderResigned(_:)),
30
+ name: UITextField.textDidEndEditingNotification,
31
+ object: input
32
+ )
33
+ } else {
34
+ input.resignFirstResponder()
35
+ }
36
+ }
37
+
38
+ @objc static func onTextInputTapped(_ gesture: UITapGestureRecognizer) {
39
+ if gesture.state == .ended {
40
+ guard let input = UIResponder.current as? TextInput else { return }
41
+
42
+ cleanup(input)
43
+
44
+ input.becomeFirstResponder()
45
+ }
46
+ }
47
+
48
+ @objc static func onResponderResigned(_ notification: Notification) {
49
+ guard let input = notification.object as? TextInput else { return }
50
+
51
+ cleanup(input)
52
+ }
53
+
54
+ static func cleanup(_ input: TextInput) {
55
+ input.inputView = nil
56
+ input.reloadInputViews()
57
+
58
+ if let gestures = input.gestureRecognizers {
59
+ for gesture in gestures where gesture.name == keyboardRevealGestureName {
60
+ input.removeGestureRecognizer(gesture)
61
+ }
62
+ }
63
+
64
+ NotificationCenter.default.removeObserver(self, name: UITextField.textDidEndEditingNotification, object: input)
65
+ }
66
+ }
@@ -0,0 +1,23 @@
1
+ //
2
+ // KeyboardEventEmitterPayload.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 07/12/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ public func buildEventParams(_ height: Double, _ duration: Int, _ tag: NSNumber) -> [AnyHashable: Any] {
12
+ var data = [AnyHashable: Any]()
13
+ let input = FocusedInputHolder.shared.get()
14
+
15
+ data["height"] = height
16
+ data["duration"] = duration
17
+ data["timestamp"] = Date.currentTimeStamp
18
+ data["target"] = tag
19
+ data["type"] = input?.keyboardType.name ?? "default"
20
+ data["appearance"] = input?.keyboardAppearance.name ?? "default"
21
+
22
+ return data
23
+ }
@@ -0,0 +1,17 @@
1
+ //
2
+ // Notification.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 04/11/2024.
6
+ //
7
+
8
+ extension Notification {
9
+ func keyboardMetaData() -> (Int, NSValue?) {
10
+ let duration = Int(
11
+ (userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0) * 1000
12
+ )
13
+ let keyboardFrame = userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
14
+
15
+ return (duration, keyboardFrame)
16
+ }
17
+ }
@@ -0,0 +1,21 @@
1
+ //
2
+ // UIKeyboardAppearance.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 08/12/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ public extension UIKeyboardAppearance {
12
+ var name: String {
13
+ switch self {
14
+ case .default: return "default"
15
+ case .dark: return "dark"
16
+ case .light: return "light"
17
+ @unknown default:
18
+ return "default"
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,30 @@
1
+ //
2
+ // UIKeyboardType.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 08/12/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ extension UIKeyboardType {
12
+ private static let keyboardTypeToStringMapping: [UIKeyboardType: String] = [
13
+ .default: "default",
14
+ .asciiCapable: "ascii-capable",
15
+ .numbersAndPunctuation: "numbers-and-punctuation",
16
+ .URL: "url",
17
+ .numberPad: "number-pad",
18
+ .phonePad: "phone-pad",
19
+ .namePhonePad: "name-phone-pad",
20
+ .emailAddress: "email-address",
21
+ .decimalPad: "decimal-pad",
22
+ .twitter: "twitter",
23
+ .webSearch: "web-search",
24
+ .asciiCapableNumberPad: "ascii-capable-number-pad",
25
+ ]
26
+
27
+ var name: String {
28
+ return UIKeyboardType.keyboardTypeToStringMapping[self] ?? "default"
29
+ }
30
+ }
@@ -163,7 +163,7 @@ public class KeyboardMovementObserver: NSObject {
163
163
  }
164
164
 
165
165
  @objc func keyboardWillAppear(_ notification: Notification) {
166
- let (duration, frame) = metaDataFromNotification(notification)
166
+ let (duration, frame) = notification.keyboardMetaData()
167
167
  if let keyboardFrame = frame {
168
168
  tag = UIResponder.current.reactViewTag
169
169
  let keyboardHeight = keyboardFrame.cgRectValue.size.height
@@ -173,7 +173,7 @@ public class KeyboardMovementObserver: NSObject {
173
173
 
174
174
  onRequestAnimation()
175
175
  onEvent("onKeyboardMoveStart", Float(keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
176
- onNotify("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight, duration))
176
+ onNotify("KeyboardController::keyboardWillShow", buildEventParams(keyboardHeight, duration, tag))
177
177
 
178
178
  setupKeyboardWatcher()
179
179
  initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
@@ -181,13 +181,13 @@ public class KeyboardMovementObserver: NSObject {
181
181
  }
182
182
 
183
183
  @objc func keyboardWillDisappear(_ notification: Notification) {
184
- let (duration, _) = metaDataFromNotification(notification)
184
+ let (duration, _) = notification.keyboardMetaData()
185
185
  tag = UIResponder.current.reactViewTag
186
186
  self.duration = duration
187
187
 
188
188
  onRequestAnimation()
189
189
  onEvent("onKeyboardMoveStart", 0, 0, duration as NSNumber, tag)
190
- onNotify("KeyboardController::keyboardWillHide", getEventParams(0, duration))
190
+ onNotify("KeyboardController::keyboardWillHide", buildEventParams(0, duration, tag))
191
191
 
192
192
  setupKeyboardWatcher()
193
193
  removeKVObserver()
@@ -196,7 +196,7 @@ public class KeyboardMovementObserver: NSObject {
196
196
 
197
197
  @objc func keyboardDidAppear(_ notification: Notification) {
198
198
  let timestamp = Date.currentTimeStamp
199
- let (duration, frame) = metaDataFromNotification(notification)
199
+ let (duration, frame) = notification.keyboardMetaData()
200
200
  if let keyboardFrame = frame {
201
201
  let (position, _) = keyboardView.frameTransitionInWindow
202
202
  let keyboardHeight = keyboardFrame.cgRectValue.size.height
@@ -210,7 +210,7 @@ public class KeyboardMovementObserver: NSObject {
210
210
 
211
211
  onCancelAnimation()
212
212
  onEvent("onKeyboardMoveEnd", height as NSNumber, progress as NSNumber, duration as NSNumber, tag)
213
- onNotify("KeyboardController::keyboardDidShow", getEventParams(height, duration))
213
+ onNotify("KeyboardController::keyboardDidShow", buildEventParams(height, duration, tag))
214
214
 
215
215
  removeKeyboardWatcher()
216
216
  setupKVObserver()
@@ -219,12 +219,12 @@ public class KeyboardMovementObserver: NSObject {
219
219
  }
220
220
 
221
221
  @objc func keyboardDidDisappear(_ notification: Notification) {
222
- let (duration, _) = metaDataFromNotification(notification)
222
+ let (duration, _) = notification.keyboardMetaData()
223
223
  tag = UIResponder.current.reactViewTag
224
224
 
225
225
  onCancelAnimation()
226
226
  onEvent("onKeyboardMoveEnd", 0 as NSNumber, 0, duration as NSNumber, tag)
227
- onNotify("KeyboardController::keyboardDidHide", getEventParams(0, duration))
227
+ onNotify("KeyboardController::keyboardDidHide", buildEventParams(0, duration, tag))
228
228
 
229
229
  removeKeyboardWatcher()
230
230
  animation = nil
@@ -309,23 +309,4 @@ public class KeyboardMovementObserver: NSObject {
309
309
  tag
310
310
  )
311
311
  }
312
-
313
- private func metaDataFromNotification(_ notification: Notification) -> (Int, NSValue?) {
314
- let duration = Int(
315
- (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0) * 1000
316
- )
317
- let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
318
-
319
- return (duration, keyboardFrame)
320
- }
321
-
322
- private func getEventParams(_ height: Double, _ duration: Int) -> [AnyHashable: Any] {
323
- var data = [AnyHashable: Any]()
324
- data["height"] = height
325
- data["duration"] = duration
326
- data["timestamp"] = Date.currentTimeStamp
327
- data["target"] = tag
328
-
329
- return data
330
- }
331
312
  }
@@ -9,7 +9,11 @@
9
9
  import Foundation
10
10
  import UIKit
11
11
 
12
- public protocol TextInput: AnyObject {
12
+ public protocol TextInput: UIView {
13
+ // default common methods/properties
14
+ var inputView: UIView? { get set }
15
+ var keyboardType: UIKeyboardType { get }
16
+ var keyboardAppearance: UIKeyboardAppearance { get }
13
17
  func focus()
14
18
  }
15
19
 
@@ -14,7 +14,10 @@ public class ViewHierarchyNavigator: NSObject {
14
14
  @objc public static func setFocusTo(direction: String) {
15
15
  DispatchQueue.main.async {
16
16
  if direction == "current" {
17
- FocusedInputHolder.shared.focus()
17
+ let input = FocusedInputHolder.shared.get()
18
+ input?.inputView = nil
19
+ input?.reloadInputViews()
20
+ input?.focus()
18
21
  return
19
22
  }
20
23