react-native-keyboard-controller 1.15.2 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +6 -0
  2. package/android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +8 -0
  3. package/android/src/main/java/com/reactnativekeyboardcontroller/extensions/EditText.kt +77 -51
  4. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputObserver.kt +3 -3
  5. package/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +2 -2
  6. package/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt +7 -0
  7. package/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +9 -1
  8. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerViewManager.kt +8 -0
  9. package/android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardGestureAreaViewManager.kt +9 -0
  10. package/ios/animations/KeyboardAnimation.swift +4 -4
  11. package/ios/animations/SpringAnimation.swift +28 -5
  12. package/ios/animations/TimingAnimation.swift +77 -32
  13. package/ios/delegates/KCTextInputCompositeDelegate.swift +73 -1
  14. package/ios/extensions/Notification.swift +4 -0
  15. package/ios/extensions/UIResponder.swift +10 -0
  16. package/ios/interactive/InvisibleInputAccessoryView.swift +59 -0
  17. package/ios/interactive/KeyboardAreaExtender.swift +74 -0
  18. package/ios/interactive/KeyboardOffsetProvider.swift +39 -0
  19. package/ios/observers/FocusedInputObserver.swift +81 -25
  20. package/ios/observers/KeyboardEventsIgnorer.swift +39 -0
  21. package/ios/observers/KeyboardMovementObserver.swift +24 -10
  22. package/ios/protocols/TextInput.swift +2 -0
  23. package/ios/swizzling/UIResponderSwizzle.swift +60 -0
  24. package/ios/traversal/ViewHierarchyNavigator.swift +1 -1
  25. package/ios/views/KeyboardGestureAreaManager.h +31 -0
  26. package/ios/views/KeyboardGestureAreaManager.mm +174 -0
  27. package/lib/commonjs/animated.js +4 -1
  28. package/lib/commonjs/animated.js.map +1 -1
  29. package/lib/commonjs/bindings.native.js +1 -1
  30. package/lib/commonjs/bindings.native.js.map +1 -1
  31. package/lib/commonjs/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  32. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js +1 -3
  33. package/lib/commonjs/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  34. package/lib/commonjs/types.js.map +1 -1
  35. package/lib/module/animated.js +4 -1
  36. package/lib/module/animated.js.map +1 -1
  37. package/lib/module/bindings.native.js +1 -1
  38. package/lib/module/bindings.native.js.map +1 -1
  39. package/lib/module/specs/KeyboardControllerViewNativeComponent.js.map +1 -1
  40. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js +1 -3
  41. package/lib/module/specs/KeyboardGestureAreaNativeComponent.js.map +1 -1
  42. package/lib/module/types.js.map +1 -1
  43. package/lib/typescript/animated.d.ts +9 -1
  44. package/lib/typescript/specs/KeyboardControllerViewNativeComponent.d.ts +1 -0
  45. package/lib/typescript/specs/KeyboardGestureAreaNativeComponent.d.ts +1 -0
  46. package/lib/typescript/types.d.ts +6 -1
  47. package/package.json +1 -1
  48. package/src/animated.tsx +15 -1
  49. package/src/bindings.native.ts +1 -1
  50. package/src/specs/KeyboardControllerViewNativeComponent.ts +1 -0
  51. package/src/specs/KeyboardGestureAreaNativeComponent.ts +4 -3
  52. package/src/types.ts +6 -1
  53. package/android/src/main/java/com/reactnativekeyboardcontroller/ui/FrameScheduler.kt +0 -43
@@ -44,6 +44,16 @@ public extension Optional where Wrapped == UIResponder {
44
44
  return (self as? UIView)?.superview?.reactTag ?? -1
45
45
  #endif
46
46
  }
47
+
48
+ var nativeID: String? {
49
+ guard let superview = (self as? UIView)?.superview else { return nil }
50
+
51
+ #if KEYBOARD_CONTROLLER_NEW_ARCH_ENABLED
52
+ return (superview as NSObject).value(forKey: "nativeId") as? String
53
+ #else
54
+ return superview.nativeID
55
+ #endif
56
+ }
47
57
  }
48
58
 
49
59
  public extension Optional where Wrapped: UIResponder {
@@ -0,0 +1,59 @@
1
+ //
2
+ // InvisibleInputAccessoryView.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 01/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ public class InvisibleInputAccessoryView: UIView {
12
+ var isShown = true
13
+
14
+ override init(frame: CGRect) {
15
+ super.init(frame: frame)
16
+ setupView()
17
+ }
18
+
19
+ public convenience init(height: CGFloat) {
20
+ self.init(frame: CGRect(x: 0, y: 0, width: 0, height: height))
21
+ }
22
+
23
+ required init?(coder aDecoder: NSCoder) {
24
+ super.init(coder: aDecoder)
25
+ setupView()
26
+ }
27
+
28
+ override public func point(inside _: CGPoint, with _: UIEvent?) -> Bool {
29
+ // Return false to allow touch events to pass through
30
+ return false
31
+ }
32
+
33
+ public func updateHeight(to newHeight: CGFloat) {
34
+ frame = CGRect(x: 0, y: 0, width: 0, height: newHeight)
35
+
36
+ // Invalidate intrinsic content size to trigger a layout update
37
+ invalidateIntrinsicContentSize()
38
+ layoutIfNeeded()
39
+ }
40
+
41
+ public func hide() {
42
+ guard isShown else { return }
43
+ isShown = false
44
+ updateHeight(to: 0.0)
45
+ superview?.layoutIfNeeded()
46
+ }
47
+
48
+ override public var intrinsicContentSize: CGSize {
49
+ return CGSize(width: UIView.noIntrinsicMetric, height: frame.height)
50
+ }
51
+
52
+ private func setupView() {
53
+ isUserInteractionEnabled = false
54
+ // for debug purposes
55
+ // backgroundColor = UIColor.red.withAlphaComponent(0.2)
56
+ backgroundColor = .clear
57
+ autoresizingMask = .flexibleHeight
58
+ }
59
+ }
@@ -0,0 +1,74 @@
1
+ //
2
+ // KeyboardAreaExtender.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 02/11/2024.
6
+ //
7
+
8
+ class KeyboardAreaExtender: NSObject {
9
+ private var currentInputAccessoryView: InvisibleInputAccessoryView?
10
+
11
+ @objc public static let shared = KeyboardAreaExtender()
12
+
13
+ override private init() {
14
+ super.init()
15
+ NotificationCenter.default.addObserver(
16
+ self,
17
+ selector: #selector(keyboardDidAppear),
18
+ name: UIResponder.keyboardDidShowNotification,
19
+ object: nil
20
+ )
21
+ }
22
+
23
+ deinit {
24
+ NotificationCenter.default.removeObserver(self)
25
+ }
26
+
27
+ public var offset: CGFloat {
28
+ return currentInputAccessoryView?.frame.height ?? 0
29
+ }
30
+
31
+ public func hide() {
32
+ if isVisible {
33
+ NotificationCenter.default.post(
34
+ name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]
35
+ )
36
+ currentInputAccessoryView?.hide()
37
+ }
38
+ }
39
+
40
+ public func remove() {
41
+ currentInputAccessoryView = nil
42
+ }
43
+
44
+ public func updateHeight(to newHeight: CGFloat, for nativeID: String) {
45
+ if UIResponder.current.nativeID == nativeID {
46
+ currentInputAccessoryView?.updateHeight(to: newHeight)
47
+ }
48
+ }
49
+
50
+ private var isVisible: Bool {
51
+ return currentInputAccessoryView?.isShown ?? false
52
+ }
53
+
54
+ @objc private func keyboardDidAppear(_: Notification) {
55
+ let responder = UIResponder.current
56
+ if let activeTextInput = responder as? TextInput,
57
+ let offset = KeyboardOffsetProvider.shared.getOffset(
58
+ forTextInputNativeID: responder.nativeID),
59
+ responder?.inputAccessoryView == nil
60
+ {
61
+ currentInputAccessoryView = InvisibleInputAccessoryView(height: CGFloat(truncating: offset))
62
+
63
+ // we need to send this event before we actually attach the IAV
64
+ // since on some OS versions (iOS 15 for example), `reloadInputViews`
65
+ // will trigger `keyboardDidAppear` listener immediately
66
+ NotificationCenter.default.post(
67
+ name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": true]
68
+ )
69
+
70
+ activeTextInput.inputAccessoryView = currentInputAccessoryView
71
+ activeTextInput.reloadInputViews()
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,39 @@
1
+ //
2
+ // KeyboardOffsetProvider.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 01/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ @objc(KeyboardOffsetProvider)
11
+ public class KeyboardOffsetProvider: NSObject {
12
+ @objc public static let shared = KeyboardOffsetProvider()
13
+
14
+ override private init() {}
15
+
16
+ private var offsetMap: [String: (offset: NSNumber, tag: NSNumber)] = [:]
17
+
18
+ @objc public func setOffset(forTextInputNativeID nativeID: String, offset: NSNumber, withTag tag: NSNumber) {
19
+ KeyboardAreaExtender.shared.updateHeight(to: CGFloat(offset), for: nativeID)
20
+ offsetMap[nativeID] = (offset: offset, tag: tag)
21
+ }
22
+
23
+ @objc public func getOffset(forTextInputNativeID nativeID: String?) -> NSNumber? {
24
+ guard let unwrappedNativeID = nativeID, let pair = offsetMap[unwrappedNativeID] else { return nil }
25
+ return pair.offset
26
+ }
27
+
28
+ @objc public func hasOffset(forTextInputNativeID nativeID: String?) -> Bool {
29
+ guard let unwrappedNativeID = nativeID else { return false }
30
+ return offsetMap[unwrappedNativeID] != nil
31
+ }
32
+
33
+ @objc public func removeOffset(forTextInputNativeID nativeID: String, withTag tag: NSNumber) {
34
+ // Ensure the tag matches before removing the entry
35
+ if let currentPair = offsetMap[nativeID], currentPair.tag == tag {
36
+ offsetMap.removeValue(forKey: nativeID)
37
+ }
38
+ }
39
+ }
@@ -37,7 +37,7 @@ public class FocusedInputObserver: NSObject {
37
37
  // input tracking
38
38
  private var currentInput: UIView?
39
39
  private var currentResponder: UIView?
40
- private var hasObservers = false
40
+ private var observation: NSKeyValueObservation?
41
41
  private var lastEventDispatched: [AnyHashable: Any] = noFocusedInputEvent
42
42
 
43
43
  @objc public init(
@@ -76,7 +76,7 @@ public class FocusedInputObserver: NSObject {
76
76
 
77
77
  NotificationCenter.default.addObserver(
78
78
  self,
79
- selector: #selector(keyboardWillShow),
79
+ selector: #selector(didReceiveFocus),
80
80
  name: UIResponder.keyboardWillShowNotification,
81
81
  object: nil
82
82
  )
@@ -86,6 +86,30 @@ public class FocusedInputObserver: NSObject {
86
86
  name: UIResponder.keyboardWillHideNotification,
87
87
  object: nil
88
88
  )
89
+ NotificationCenter.default.addObserver(
90
+ self,
91
+ selector: #selector(didReceiveFocus),
92
+ name: UITextField.textDidBeginEditingNotification,
93
+ object: nil
94
+ )
95
+ NotificationCenter.default.addObserver(
96
+ self,
97
+ selector: #selector(didReceiveFocus),
98
+ name: UITextView.textDidBeginEditingNotification,
99
+ object: nil
100
+ )
101
+ NotificationCenter.default.addObserver(
102
+ self,
103
+ selector: #selector(didReceiveBlur),
104
+ name: UITextField.textDidEndEditingNotification,
105
+ object: nil
106
+ )
107
+ NotificationCenter.default.addObserver(
108
+ self,
109
+ selector: #selector(didReceiveBlur),
110
+ name: UITextView.textDidEndEditingNotification,
111
+ object: nil
112
+ )
89
113
  }
90
114
 
91
115
  @objc public func unmount() {
@@ -94,7 +118,41 @@ public class FocusedInputObserver: NSObject {
94
118
  NotificationCenter.default.removeObserver(self)
95
119
  }
96
120
 
97
- @objc func keyboardWillShow(_: Notification) {
121
+ @objc func keyboardWillHide(_: Notification) {
122
+ onBlur()
123
+ }
124
+
125
+ @objc func didReceiveFocus(_: Notification) {
126
+ if UIResponder.current == currentResponder {
127
+ // focus was already handled by keyboard event
128
+ return
129
+ }
130
+
131
+ onFocus()
132
+ }
133
+
134
+ /**
135
+ * We handle blur events in `keyboardWillHide` handler
136
+ * And this additional handler is needed only to have
137
+ * a consistent state when keyboard is not shown
138
+ */
139
+ @objc func didReceiveBlur(_: Notification) {
140
+ if currentResponder == nil {
141
+ // blur was already handled by keyboard event
142
+ return
143
+ }
144
+ // blur gets triggered on endEditing, so we check if no upcoming
145
+ // didReceiveFocus events are coming to exclude `noFocusedInput`
146
+ // event when user switches between inputs
147
+ DispatchQueue.main.async {
148
+ // check that it wasn't a switch between inputs
149
+ if self.currentResponder == nil {
150
+ self.onBlur()
151
+ }
152
+ }
153
+ }
154
+
155
+ func onFocus() {
98
156
  guard let responder = UIResponder.current as? UIView else { return }
99
157
  removeObservers(newResponder: responder)
100
158
  currentResponder = responder
@@ -106,7 +164,7 @@ public class FocusedInputObserver: NSObject {
106
164
  FocusedInputHolder.shared.set(currentResponder as? TextInput)
107
165
 
108
166
  let allInputFields = ViewHierarchyNavigator.getAllInputFields()
109
- let currentIndex = allInputFields.firstIndex(where: { $0 as? UIView == currentResponder }) ?? -1
167
+ let currentIndex = allInputFields.firstIndex(where: { $0 == currentResponder }) ?? -1
110
168
 
111
169
  onFocusDidSet([
112
170
  "current": currentIndex,
@@ -114,7 +172,11 @@ public class FocusedInputObserver: NSObject {
114
172
  ])
115
173
  }
116
174
 
117
- @objc func keyboardWillHide(_: Notification) {
175
+ func onBlur() {
176
+ // when we switch to next input, but `showSoftInput={false}`
177
+ if UIResponder.current is TextInput {
178
+ return
179
+ }
118
180
  removeObservers(newResponder: nil)
119
181
  currentInput = nil
120
182
  currentResponder = nil
@@ -162,25 +224,27 @@ public class FocusedInputObserver: NSObject {
162
224
  }
163
225
 
164
226
  private func setupObservers() {
165
- if hasObservers {
227
+ if observation != nil {
166
228
  return
167
229
  }
168
230
 
169
231
  if currentInput != nil {
170
- hasObservers = true
171
- currentInput?.addObserver(self, forKeyPath: "center", options: .new, context: nil)
232
+ observation = currentInput?.observe(\.center, options: .new) { [weak self] view, change in
233
+ guard let self = self else { return }
234
+ self.onLayoutChange(view: view, change: change)
235
+ }
172
236
 
173
237
  substituteDelegate(currentResponder)
174
238
  }
175
239
  }
176
240
 
177
241
  private func removeObservers(newResponder: UIResponder?) {
178
- if newResponder == currentResponder || !hasObservers {
242
+ if newResponder == currentResponder || observation == nil {
179
243
  return
180
244
  }
181
245
 
182
- hasObservers = false
183
- currentInput?.removeObserver(self, forKeyPath: "center", context: nil)
246
+ observation?.invalidate()
247
+ observation = nil
184
248
 
185
249
  substituteDelegateBack(currentResponder)
186
250
  }
@@ -188,7 +252,7 @@ public class FocusedInputObserver: NSObject {
188
252
  private func substituteDelegate(_ input: UIResponder?) {
189
253
  if let textField = input as? UITextField {
190
254
  if !(textField.delegate is KCTextInputCompositeDelegate) {
191
- delegate.setTextFieldDelegate(delegate: textField.delegate)
255
+ delegate.setTextFieldDelegate(delegate: textField.delegate, textField: textField)
192
256
  textField.delegate = delegate
193
257
  }
194
258
  } else if let textView = input as? UITextView {
@@ -211,19 +275,11 @@ public class FocusedInputObserver: NSObject {
211
275
  }
212
276
  }
213
277
 
214
- // swiftlint:disable:next block_based_kvo
215
- @objc override public func observeValue(
216
- forKeyPath keyPath: String?,
217
- of object: Any?,
218
- change _: [NSKeyValueChangeKey: Any]?,
219
- context _: UnsafeMutableRawPointer?
220
- ) {
221
- if keyPath == "center", object as? NSObject == currentInput {
222
- // we need to read layout in next frame, otherwise we'll get old
223
- // layout values
224
- DispatchQueue.main.async {
225
- self.syncUpLayout()
226
- }
278
+ private func onLayoutChange(view _: UIView, change _: NSKeyValueObservedChange<CGPoint>) {
279
+ // we need to read layout in next frame, otherwise we'll get old
280
+ // layout values
281
+ DispatchQueue.main.async {
282
+ self.syncUpLayout()
227
283
  }
228
284
  }
229
285
 
@@ -0,0 +1,39 @@
1
+ //
2
+ // KeyboardEventsIgnorer.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 24/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ @objc(KeyboardEventsIgnorer)
11
+ public class KeyboardEventsIgnorer: NSObject {
12
+ @objc public static let shared = KeyboardEventsIgnorer()
13
+
14
+ var shouldIgnoreKeyboardEvents = false
15
+
16
+ public var shouldIgnore: Bool {
17
+ return shouldIgnoreKeyboardEvents
18
+ }
19
+
20
+ override init() {
21
+ super.init()
22
+ NotificationCenter.default.addObserver(
23
+ self,
24
+ selector: #selector(handleIgnoreKeyboardEventsNotification),
25
+ name: .shouldIgnoreKeyboardEvents,
26
+ object: nil
27
+ )
28
+ }
29
+
30
+ @objc private func handleIgnoreKeyboardEventsNotification(_ notification: Notification) {
31
+ if let userInfo = notification.userInfo, let value = userInfo["ignore"] as? Bool {
32
+ shouldIgnoreKeyboardEvents = value
33
+ }
34
+ }
35
+
36
+ deinit {
37
+ NotificationCenter.default.removeObserver(self)
38
+ }
39
+ }
@@ -36,7 +36,12 @@ public class KeyboardMovementObserver: NSObject {
36
36
  private var hasKVObserver = false
37
37
  private var isMounted = false
38
38
  // state variables
39
- private var keyboardHeight: CGFloat = 0.0
39
+ private var _keyboardHeight: CGFloat = 0.0
40
+ private var keyboardHeight: CGFloat {
41
+ get { _keyboardHeight - KeyboardAreaExtender.shared.offset }
42
+ set { _keyboardHeight = newValue }
43
+ }
44
+
40
45
  private var duration = 0
41
46
  private var tag: NSNumber = -1
42
47
  private var animation: KeyboardAnimation?
@@ -121,7 +126,7 @@ public class KeyboardMovementObserver: NSObject {
121
126
  }
122
127
  // if keyboard height is not equal to its bounds - we can ignore
123
128
  // values, since they'll be invalid and will cause UI jumps
124
- if keyboardView?.bounds.size.height != keyboardHeight {
129
+ if floor(keyboardView?.bounds.size.height ?? 0) != floor(_keyboardHeight) {
125
130
  return
126
131
  }
127
132
 
@@ -131,11 +136,12 @@ public class KeyboardMovementObserver: NSObject {
131
136
  let keyboardFrameY = changeValue.cgPointValue.y
132
137
  let keyboardWindowH = keyboardView?.window?.bounds.size.height ?? 0
133
138
  let keyboardPosition = keyboardWindowH - keyboardFrameY
139
+
134
140
  let position = CGFloat.interpolate(
135
- inputRange: [keyboardHeight / 2, -keyboardHeight / 2],
136
- outputRange: [keyboardHeight, 0],
141
+ inputRange: [_keyboardHeight / 2, -_keyboardHeight / 2],
142
+ outputRange: [_keyboardHeight, 0],
137
143
  currentValue: keyboardPosition
138
- )
144
+ ) - KeyboardAreaExtender.shared.offset
139
145
 
140
146
  if position == 0 {
141
147
  // it will be triggered before `keyboardWillDisappear` and
@@ -163,6 +169,8 @@ public class KeyboardMovementObserver: NSObject {
163
169
  }
164
170
 
165
171
  @objc func keyboardWillAppear(_ notification: Notification) {
172
+ guard !KeyboardEventsIgnorer.shared.shouldIgnore else { return }
173
+
166
174
  let (duration, frame) = notification.keyboardMetaData()
167
175
  if let keyboardFrame = frame {
168
176
  tag = UIResponder.current.reactViewTag
@@ -172,11 +180,11 @@ public class KeyboardMovementObserver: NSObject {
172
180
  didShowDeadline = Date.currentTimeStamp + Int64(duration)
173
181
 
174
182
  onRequestAnimation()
175
- onEvent("onKeyboardMoveStart", Float(keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
176
- onNotify("KeyboardController::keyboardWillShow", buildEventParams(keyboardHeight, duration, tag))
183
+ onEvent("onKeyboardMoveStart", Float(self.keyboardHeight) as NSNumber, 1, duration as NSNumber, tag)
184
+ onNotify("KeyboardController::keyboardWillShow", buildEventParams(self.keyboardHeight, duration, tag))
177
185
 
178
186
  setupKeyboardWatcher()
179
- initializeAnimation(fromValue: prevKeyboardPosition, toValue: keyboardHeight)
187
+ initializeAnimation(fromValue: prevKeyboardPosition, toValue: self.keyboardHeight)
180
188
  }
181
189
  }
182
190
 
@@ -202,9 +210,15 @@ public class KeyboardMovementObserver: NSObject {
202
210
  let keyboardHeight = keyboardFrame.cgRectValue.size.height
203
211
  tag = UIResponder.current.reactViewTag
204
212
  self.keyboardHeight = keyboardHeight
213
+
214
+ guard !KeyboardEventsIgnorer.shared.shouldIgnore else {
215
+ KeyboardEventsIgnorer.shared.shouldIgnoreKeyboardEvents = false
216
+ return
217
+ }
218
+
205
219
  // if the event is caught in between it's highly likely that it could be a "resize" event
206
220
  // so we just read actual keyboard frame value in this case
207
- let height = timestamp >= didShowDeadline ? keyboardHeight : position
221
+ let height = timestamp >= didShowDeadline ? self.keyboardHeight : position - KeyboardAreaExtender.shared.offset
208
222
  // always limit progress to the maximum possible value
209
223
  let progress = min(height / self.keyboardHeight, 1.0)
210
224
 
@@ -267,7 +281,7 @@ public class KeyboardMovementObserver: NSObject {
267
281
  }
268
282
 
269
283
  let (visibleKeyboardHeight, keyboardFrameY) = keyboardView.frameTransitionInWindow
270
- var keyboardPosition = visibleKeyboardHeight
284
+ var keyboardPosition = visibleKeyboardHeight - KeyboardAreaExtender.shared.offset
271
285
 
272
286
  if keyboardPosition == prevKeyboardPosition || keyboardFrameY == 0 {
273
287
  return
@@ -11,9 +11,11 @@ import UIKit
11
11
 
12
12
  public protocol TextInput: UIView {
13
13
  // default common methods/properties
14
+ var inputAccessoryView: UIView? { get set }
14
15
  var inputView: UIView? { get set }
15
16
  var keyboardType: UIKeyboardType { get }
16
17
  var keyboardAppearance: UIKeyboardAppearance { get }
18
+ // custom methods/properties
17
19
  func focus()
18
20
  }
19
21
 
@@ -0,0 +1,60 @@
1
+ //
2
+ // UIResponderSwizzle.swift
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 01/11/2024.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ private var originalResignFirstResponder: IMP?
12
+
13
+ @objc
14
+ extension UIResponder {
15
+ public static func swizzleResignFirstResponder() {
16
+ let originalSelector = #selector(resignFirstResponder)
17
+
18
+ guard let originalMethod = class_getInstanceMethod(UIResponder.self, originalSelector) else {
19
+ return
20
+ }
21
+
22
+ originalResignFirstResponder = method_getImplementation(originalMethod)
23
+
24
+ let swizzledImplementation: @convention(block) (UIResponder) -> Bool = { (self) in
25
+ // Check the type of responder
26
+ if let textField = self as? TextInput {
27
+ // check inputAccessoryView and call original method immediately if not InvisibleInputAccessoryView
28
+ if !(textField.inputAccessoryView is InvisibleInputAccessoryView) {
29
+ return self.callOriginalResignFirstResponder(originalSelector)
30
+ }
31
+ } else {
32
+ // If casting to TextInput fails
33
+ return self.callOriginalResignFirstResponder(originalSelector)
34
+ }
35
+
36
+ KeyboardAreaExtender.shared.hide()
37
+
38
+ // Postpone execution of the original resignFirstResponder
39
+ DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) {
40
+ (self as? TextInput)?.inputAccessoryView = nil
41
+ KeyboardAreaExtender.shared.remove()
42
+ _ = self.callOriginalResignFirstResponder(originalSelector)
43
+ }
44
+
45
+ // We need to return a value immediately, even though the actual action is delayed
46
+ return false
47
+ }
48
+
49
+ let implementation = imp_implementationWithBlock(swizzledImplementation)
50
+ method_setImplementation(originalMethod, implementation)
51
+ }
52
+
53
+ private func callOriginalResignFirstResponder(_ selector: Selector) -> Bool {
54
+ guard let originalResignFirstResponder = originalResignFirstResponder else { return false }
55
+ typealias Function = @convention(c) (AnyObject, Selector) -> Bool
56
+ let castOriginalResignFirstResponder = unsafeBitCast(originalResignFirstResponder, to: Function.self)
57
+ let result = castOriginalResignFirstResponder(self, selector)
58
+ return result
59
+ }
60
+ }
@@ -21,7 +21,7 @@ public class ViewHierarchyNavigator: NSObject {
21
21
  return
22
22
  }
23
23
 
24
- let input = (FocusedInputHolder.shared.get() as? UIView) ?? (UIResponder.current as? UIView) ?? nil
24
+ let input = FocusedInputHolder.shared.get() ?? (UIResponder.current as? UIView) ?? nil
25
25
  guard let view = input else { return }
26
26
 
27
27
  let textField = findTextInputInDirection(currentFocus: view, direction: direction)
@@ -0,0 +1,31 @@
1
+ //
2
+ // KeyboardGestureAreaManager.h
3
+ // Pods
4
+ //
5
+ // Created by Kiryl Ziusko on 01/11/2024.
6
+ //
7
+
8
+ #ifdef RCT_NEW_ARCH_ENABLED
9
+ #import <React/RCTViewComponentView.h>
10
+ #else
11
+ #import <React/RCTBridge.h>
12
+ #endif
13
+ #import <React/RCTViewManager.h>
14
+ #import <UIKit/UIKit.h>
15
+
16
+ @interface KeyboardGestureAreaManager : RCTViewManager
17
+ @end
18
+
19
+ @interface KeyboardGestureArea :
20
+ #ifdef RCT_NEW_ARCH_ENABLED
21
+ RCTViewComponentView
22
+ #else
23
+ UIView
24
+
25
+ - (instancetype)initWithBridge:(RCTBridge *)bridge;
26
+
27
+ #endif
28
+
29
+ @property (nonatomic, strong) NSNumber *offset;
30
+ @property (nonatomic, strong) NSString *textInputNativeID;
31
+ @end