diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt index 8bffcae..cdb9a3a 100644 --- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt @@ -54,7 +54,7 @@ class KeyboardControllerModuleImpl( view.clearFocus() } } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !animated) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !animated && !controller.isInsetAnimationInProgress()) { controller.startControlRequest(view) { insetsController -> insetsController.finish(false) diff --git a/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift b/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift index 32ae6de..ace9817 100644 --- a/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift +++ b/node_modules/react-native-keyboard-controller/ios/swizzling/UIResponderSwizzle.swift @@ -9,6 +9,7 @@ import Foundation import UIKit private var pendingBecomeResponder: TextInput? +private weak var pendingResignResponder: UIResponder? private var originalResignFirstResponder: IMP? private var originalBecomeFirstResponder: IMP? @@ -69,9 +70,13 @@ extension UIResponder { } KeyboardAreaExtender.shared.hide() + pendingResignResponder = self // Postpone execution of the original resignFirstResponder DispatchQueue.main.asyncAfter(deadline: .now() + UIUtils.nextFrame) { + if pendingResignResponder === self { + pendingResignResponder = nil + } NotificationCenter.default.post( name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": false] ) @@ -86,6 +91,45 @@ extension UIResponder { let implementation = imp_implementationWithBlock(swizzledResignImplementation) method_setImplementation(originalResignMethod, implementation) + + swizzleWillMoveToWindow(originalResignSelector) + } + + /// If an input with a postponed resign is about to leave its window (native-stack pop on iOS 26+, + /// unmount, etc.), then the postponed resign will fail, because input will not be attached to the + /// window anymore, and window will keep a stale first responder. So we resign it immediately while + /// input is still attached. + private static func swizzleWillMoveToWindow(_ resignSelector: Selector) { + let selector = #selector(UIView.willMove(toWindow:)) + + for cls in [UITextView.self, UITextField.self] as [AnyClass] { + guard let method = class_getInstanceMethod(cls, selector) else { continue } + + // may be inherited from `UIView` - we add own method to the class in this case, + // so the change doesn't affect all views + let originalImplementation = method_getImplementation(method) + typealias Function = @convention(c) (AnyObject, Selector, UIWindow?) -> Void + let castOriginalImplementation = unsafeBitCast(originalImplementation, to: Function.self) + + let swizzledImplementation: @convention(block) (UIView, UIWindow?) -> Void = { view, newWindow in + if newWindow !== view.window, pendingResignResponder === view { + pendingResignResponder = nil + NotificationCenter.default.post( + name: .shouldIgnoreKeyboardEvents, object: nil, userInfo: ["ignore": false] + ) + (view as? TextInput)?.inputAccessoryView = nil + KeyboardAreaExtender.shared.remove() + _ = view.callOriginalResignFirstResponder(resignSelector) + } + + castOriginalImplementation(view, selector, newWindow) + } + + let implementation = imp_implementationWithBlock(swizzledImplementation) + if !class_addMethod(cls, selector, implementation, method_getTypeEncoding(method)) { + method_setImplementation(method, implementation) + } + } } private func callOriginalResignFirstResponder(_ selector: Selector) -> Bool {