react-native-keyboard-controller 1.17.3 → 1.17.4

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.
@@ -18,17 +18,21 @@ class KeyboardControllerViewManager(
18
18
  private val manager = KeyboardControllerViewManagerImpl(mReactContext)
19
19
  private val mDelegate = KeyboardControllerViewManagerDelegate(this)
20
20
 
21
- override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = mDelegate
22
-
23
- override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
24
-
21
+ // region Lifecycle
25
22
  override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = manager.createViewInstance(context)
26
23
 
24
+ override fun invalidate() {
25
+ super.invalidate()
26
+ manager.invalidate()
27
+ }
28
+
27
29
  override fun onAfterUpdateTransaction(view: ReactViewGroup) {
28
30
  super.onAfterUpdateTransaction(view)
29
31
  manager.setEdgeToEdge(view as EdgeToEdgeReactViewGroup)
30
32
  }
33
+ // endregion
31
34
 
35
+ // region Props setters
32
36
  @ReactProp(name = "statusBarTranslucent")
33
37
  override fun setStatusBarTranslucent(
34
38
  view: ReactViewGroup,
@@ -52,7 +56,14 @@ class KeyboardControllerViewManager(
52
56
  view: ReactViewGroup,
53
57
  value: Boolean,
54
58
  ) = manager.setEnabled(view as EdgeToEdgeReactViewGroup, value)
59
+ // endregion
55
60
 
61
+ // region Getters
56
62
  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
57
63
  manager.getExportedCustomDirectEventTypeConstants()
64
+
65
+ override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = mDelegate
66
+
67
+ override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
68
+ // endregion
58
69
  }
@@ -10,13 +10,8 @@ import com.facebook.react.uimanager.ThemedReactContext
10
10
  import com.facebook.react.uimanager.UIManagerHelper
11
11
  import com.facebook.react.uimanager.events.Event
12
12
  import com.facebook.react.uimanager.events.EventDispatcher
13
- import com.reactnativekeyboardcontroller.listeners.WindowDimensionListener
14
13
  import com.reactnativekeyboardcontroller.log.Logger
15
14
 
16
- fun ThemedReactContext.setupWindowDimensionsListener() {
17
- WindowDimensionListener(this)
18
- }
19
-
20
15
  fun ThemedReactContext?.dispatchEvent(
21
16
  viewId: Int,
22
17
  event: Event<*>,
@@ -1,6 +1,7 @@
1
1
  package com.reactnativekeyboardcontroller.listeners
2
2
 
3
3
  import android.view.ViewGroup
4
+ import android.view.ViewTreeObserver
4
5
  import com.facebook.react.bridge.Arguments
5
6
  import com.facebook.react.uimanager.ThemedReactContext
6
7
  import com.reactnativekeyboardcontroller.extensions.content
@@ -16,8 +17,9 @@ class WindowDimensionListener(
16
17
  private val context: ThemedReactContext?,
17
18
  ) {
18
19
  private var lastDispatchedDimensions = Dimensions(0.0, 0.0)
20
+ private var layoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
19
21
 
20
- init {
22
+ public fun attachListener() {
21
23
  // attach to content view only once per app instance
22
24
  if (context != null && listenerID != context.hashCode()) {
23
25
  listenerID = context.hashCode()
@@ -26,12 +28,19 @@ class WindowDimensionListener(
26
28
 
27
29
  updateWindowDimensions(content)
28
30
 
29
- content?.viewTreeObserver?.addOnGlobalLayoutListener {
30
- updateWindowDimensions(content)
31
- }
31
+ layoutListener =
32
+ ViewTreeObserver.OnGlobalLayoutListener {
33
+ updateWindowDimensions(content)
34
+ }
35
+
36
+ content?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
32
37
  }
33
38
  }
34
39
 
40
+ public fun detachListener() {
41
+ context?.content?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
42
+ }
43
+
35
44
  private fun updateWindowDimensions(content: ViewGroup?) {
36
45
  if (content == null) {
37
46
  return
@@ -7,14 +7,26 @@ import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEvent
7
7
  import com.reactnativekeyboardcontroller.events.FocusedInputSelectionChangedEvent
8
8
  import com.reactnativekeyboardcontroller.events.FocusedInputTextChangedEvent
9
9
  import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent
10
+ import com.reactnativekeyboardcontroller.listeners.WindowDimensionListener
10
11
  import com.reactnativekeyboardcontroller.views.EdgeToEdgeReactViewGroup
11
12
 
12
13
  @Suppress("detekt:UnusedPrivateProperty")
13
14
  class KeyboardControllerViewManagerImpl(
14
15
  mReactContext: ReactApplicationContext,
15
16
  ) {
16
- fun createViewInstance(reactContext: ThemedReactContext): EdgeToEdgeReactViewGroup =
17
- EdgeToEdgeReactViewGroup(reactContext)
17
+ private var listener: WindowDimensionListener? = null
18
+
19
+ fun createViewInstance(reactContext: ThemedReactContext): EdgeToEdgeReactViewGroup {
20
+ if (listener == null) {
21
+ listener = WindowDimensionListener(reactContext)
22
+ listener?.attachListener()
23
+ }
24
+ return EdgeToEdgeReactViewGroup(reactContext)
25
+ }
26
+
27
+ fun invalidate() {
28
+ listener?.detachListener()
29
+ }
18
30
 
19
31
  fun setEnabled(
20
32
  view: EdgeToEdgeReactViewGroup,
@@ -15,7 +15,6 @@ import com.reactnativekeyboardcontroller.extensions.content
15
15
  import com.reactnativekeyboardcontroller.extensions.removeSelf
16
16
  import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
17
17
  import com.reactnativekeyboardcontroller.extensions.rootView
18
- import com.reactnativekeyboardcontroller.extensions.setupWindowDimensionsListener
19
18
  import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
20
19
  import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallbackConfig
21
20
  import com.reactnativekeyboardcontroller.log.Logger
@@ -51,7 +50,6 @@ class EdgeToEdgeReactViewGroup(
51
50
  private val modalAttachedWatcher = ModalAttachedWatcher(this, reactContext, config, ::getKeyboardCallback)
52
51
 
53
52
  init {
54
- reactContext.setupWindowDimensionsListener()
55
53
  tag = VIEW_TAG
56
54
  }
57
55
 
@@ -65,13 +63,13 @@ class EdgeToEdgeReactViewGroup(
65
63
  return
66
64
  }
67
65
 
68
- this.setupKeyboardCallbacks()
66
+ this.activate()
69
67
  }
70
68
 
71
69
  override fun onDetachedFromWindow() {
72
70
  super.onDetachedFromWindow()
73
71
 
74
- this.removeKeyboardCallbacks()
72
+ this.deactivate()
75
73
  }
76
74
 
77
75
  override fun onConfigurationChanged(newConfig: Configuration?) {
@@ -189,12 +187,20 @@ class EdgeToEdgeReactViewGroup(
189
187
  // region State managers
190
188
  private fun enable() {
191
189
  this.setupWindowInsets()
192
- this.setupKeyboardCallbacks()
193
- modalAttachedWatcher.enable()
190
+ this.activate()
194
191
  }
195
192
 
196
193
  private fun disable() {
197
194
  this.setupWindowInsets()
195
+ this.deactivate()
196
+ }
197
+
198
+ private fun activate() {
199
+ this.setupKeyboardCallbacks()
200
+ modalAttachedWatcher.enable()
201
+ }
202
+
203
+ private fun deactivate() {
198
204
  this.removeKeyboardCallbacks()
199
205
  modalAttachedWatcher.disable()
200
206
  }
@@ -6,6 +6,7 @@ import android.graphics.PixelFormat
6
6
  import android.view.MotionEvent
7
7
  import android.view.View
8
8
  import android.view.WindowManager
9
+ import android.view.accessibility.AccessibilityEvent
9
10
  import com.facebook.react.bridge.UiThreadUtil
10
11
  import com.facebook.react.bridge.WritableMap
11
12
  import com.facebook.react.bridge.WritableNativeMap
@@ -84,6 +85,16 @@ class OverKeyboardHostView(
84
85
  }
85
86
  // endregion
86
87
 
88
+ // region Accessibility
89
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
90
+ // Don’t let Android try to reach children that are not actual descendants
91
+ }
92
+
93
+ // Explicitly override this to prevent accessibility events being passed down to children
94
+ // Those will be handled by the `hostView` which lives in the `windowManager`
95
+ override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
96
+ // endregion
97
+
87
98
  fun show() {
88
99
  val layoutParams =
89
100
  WindowManager.LayoutParams(
@@ -13,16 +13,21 @@ class KeyboardControllerViewManager(
13
13
  ) : ReactViewManager() {
14
14
  private val manager = KeyboardControllerViewManagerImpl(mReactContext)
15
15
 
16
- override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
16
+ // region Lifecycle
17
+ override fun createViewInstance(context: ThemedReactContext): ReactViewGroup = manager.createViewInstance(context)
17
18
 
18
- override fun createViewInstance(reactContext: ThemedReactContext): EdgeToEdgeReactViewGroup =
19
- manager.createViewInstance(reactContext)
19
+ override fun invalidate() {
20
+ super.invalidate()
21
+ manager.invalidate()
22
+ }
20
23
 
21
24
  override fun onAfterUpdateTransaction(view: ReactViewGroup) {
22
25
  super.onAfterUpdateTransaction(view)
23
26
  manager.setEdgeToEdge(view as EdgeToEdgeReactViewGroup)
24
27
  }
28
+ // endregion
25
29
 
30
+ // region Props setters
26
31
  @ReactProp(name = "enabled")
27
32
  fun setEnabled(
28
33
  view: EdgeToEdgeReactViewGroup,
@@ -54,7 +59,12 @@ class KeyboardControllerViewManager(
54
59
  ) {
55
60
  manager.setPreserveEdgeToEdge(view, isPreservingEdgeToEdge)
56
61
  }
62
+ // endregion
57
63
 
64
+ // region Getters
58
65
  override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> =
59
66
  manager.getExportedCustomDirectEventTypeConstants()
67
+
68
+ override fun getName(): String = KeyboardControllerViewManagerImpl.NAME
69
+ // endregion
60
70
  }
@@ -8,29 +8,64 @@
8
8
  import Foundation
9
9
  import UIKit
10
10
 
11
+ private var pendingBecomeResponder: TextInput?
11
12
  private var originalResignFirstResponder: IMP?
13
+ private var originalBecomeFirstResponder: IMP?
12
14
 
13
15
  @objc
14
16
  extension UIResponder {
17
+ public static func swizzleBecomeFirstResponder() {
18
+ let originalBecomeSelector = #selector(becomeFirstResponder)
19
+ guard
20
+ let originalBecomeMethod = class_getInstanceMethod(UIResponder.self, originalBecomeSelector)
21
+ else {
22
+ return
23
+ }
24
+
25
+ originalBecomeFirstResponder = method_getImplementation(originalBecomeMethod)
26
+
27
+ let swizzledBecomeImplementation: @convention(block) (UIResponder) -> Bool = { (self) in
28
+ pendingBecomeResponder = self as? TextInput
29
+
30
+ DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) {
31
+ pendingBecomeResponder = nil
32
+ }
33
+
34
+ return self.callOriginalBecomeFirstResponder(originalBecomeSelector)
35
+ }
36
+
37
+ let implementation = imp_implementationWithBlock(swizzledBecomeImplementation)
38
+ method_setImplementation(originalBecomeMethod, implementation)
39
+ }
40
+
15
41
  public static func swizzleResignFirstResponder() {
16
- let originalSelector = #selector(resignFirstResponder)
42
+ let originalResignSelector = #selector(resignFirstResponder)
17
43
 
18
- guard let originalMethod = class_getInstanceMethod(UIResponder.self, originalSelector) else {
44
+ guard
45
+ let originalResignMethod = class_getInstanceMethod(UIResponder.self, originalResignSelector)
46
+ else {
19
47
  return
20
48
  }
21
49
 
22
- originalResignFirstResponder = method_getImplementation(originalMethod)
50
+ originalResignFirstResponder = method_getImplementation(originalResignMethod)
23
51
 
24
- let swizzledImplementation: @convention(block) (UIResponder) -> Bool = { (self) in
52
+ let swizzledResignImplementation: @convention(block) (UIResponder) -> Bool = { (self) in
25
53
  // Check the type of responder
26
54
  if let textField = self as? TextInput {
27
55
  // check inputAccessoryView and call original method immediately if not InvisibleInputAccessoryView
28
56
  if !(textField.inputAccessoryView is InvisibleInputAccessoryView) {
29
- return self.callOriginalResignFirstResponder(originalSelector)
57
+ return self.callOriginalResignFirstResponder(originalResignSelector)
58
+ } else if pendingBecomeResponder != nil {
59
+ // if we already have a new focus request
60
+ pendingBecomeResponder = nil
61
+ KeyboardAreaExtender.shared.hide()
62
+ (self as? TextInput)?.inputAccessoryView = nil
63
+ KeyboardAreaExtender.shared.remove()
64
+ return self.callOriginalResignFirstResponder(originalResignSelector)
30
65
  }
31
66
  } else {
32
67
  // If casting to TextInput fails
33
- return self.callOriginalResignFirstResponder(originalSelector)
68
+ return self.callOriginalResignFirstResponder(originalResignSelector)
34
69
  }
35
70
 
36
71
  KeyboardAreaExtender.shared.hide()
@@ -39,22 +74,34 @@ extension UIResponder {
39
74
  DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) {
40
75
  (self as? TextInput)?.inputAccessoryView = nil
41
76
  KeyboardAreaExtender.shared.remove()
42
- _ = self.callOriginalResignFirstResponder(originalSelector)
77
+ _ = self.callOriginalResignFirstResponder(originalResignSelector)
43
78
  }
44
79
 
45
80
  // We need to return a value immediately, even though the actual action is delayed
46
81
  return false
47
82
  }
48
83
 
49
- let implementation = imp_implementationWithBlock(swizzledImplementation)
50
- method_setImplementation(originalMethod, implementation)
84
+ let implementation = imp_implementationWithBlock(swizzledResignImplementation)
85
+ method_setImplementation(originalResignMethod, implementation)
51
86
  }
52
87
 
53
88
  private func callOriginalResignFirstResponder(_ selector: Selector) -> Bool {
54
89
  guard let originalResignFirstResponder = originalResignFirstResponder else { return false }
55
90
  typealias Function = @convention(c) (AnyObject, Selector) -> Bool
56
- let castOriginalResignFirstResponder = unsafeBitCast(originalResignFirstResponder, to: Function.self)
91
+ let castOriginalResignFirstResponder = unsafeBitCast(
92
+ originalResignFirstResponder, to: Function.self
93
+ )
57
94
  let result = castOriginalResignFirstResponder(self, selector)
58
95
  return result
59
96
  }
97
+
98
+ private func callOriginalBecomeFirstResponder(_ selector: Selector) -> Bool {
99
+ guard let originalBecomeFirstResponder = originalBecomeFirstResponder else { return false }
100
+ typealias Function = @convention(c) (AnyObject, Selector) -> Bool
101
+ let castOriginalBecomeFirstResponder = unsafeBitCast(
102
+ originalBecomeFirstResponder, to: Function.self
103
+ )
104
+ let result = castOriginalBecomeFirstResponder(self, selector)
105
+ return result
106
+ }
60
107
  }
@@ -75,6 +75,7 @@ RCT_EXPORT_VIEW_PROPERTY(offset, NSNumber *)
75
75
  [super load];
76
76
 
77
77
  [UIResponder swizzleResignFirstResponder];
78
+ [UIResponder swizzleBecomeFirstResponder];
78
79
  }
79
80
 
80
81
  // MARK: Constructor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-keyboard-controller",
3
- "version": "1.17.3",
3
+ "version": "1.17.4",
4
4
  "description": "Keyboard manager which works in identical way on both iOS and Android",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",