Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion FabricExample/src/screens/Examples/Close/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function CloseScreen() {

const ref = useRef<TextInput>(null);
const [keepFocus, setKeepFocus] = useState(false);
const [animated, setAnimated] = useState(true);

return (
<View>
Expand All @@ -18,6 +19,11 @@ function CloseScreen() {
title={keepFocus ? "Keep focus" : "Don't keep focus"}
onPress={() => setKeepFocus(!keepFocus)}
/>
<Button
testID="animated_button"
title={animated ? "Animated" : "Instant"}
onPress={() => setAnimated(!animated)}
/>
<Button
testID="set_focus_to_current"
title="KeyboardController.setFocusTo('current')"
Expand All @@ -36,7 +42,7 @@ function CloseScreen() {
<Button
testID="close_keyboard_button"
title="Close keyboard"
onPress={() => KeyboardController.dismiss({ keepFocus })}
onPress={() => KeyboardController.dismiss({ keepFocus, animated })}
/>
<TextInput
ref={ref}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ class KeyboardControllerModule(
module.preload()
}

override fun dismiss(keepFocus: Boolean) {
module.dismiss(keepFocus)
override fun dismiss(
keepFocus: Boolean,
animated: Boolean,
) {
module.dismiss(keepFocus, animated)
}

override fun setFocusTo(direction: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package com.reactnativekeyboardcontroller.modules

import android.content.Context
import android.os.Build
import android.view.View
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
import com.reactnativekeyboardcontroller.interactive.KeyboardAnimationController
import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator

class KeyboardControllerModuleImpl(
private val mReactContext: ReactApplicationContext,
) {
private val controller = KeyboardAnimationController()
private val mDefaultMode: Int = getCurrentMode()

fun setInputMode(mode: Int) {
Expand All @@ -26,16 +29,33 @@ class KeyboardControllerModuleImpl(
// no-op on Android
}

fun dismiss(keepFocus: Boolean) {
fun dismiss(
keepFocus: Boolean,
animated: Boolean,
) {
val activity = mReactContext.currentActivity
val view: View? = FocusedInputHolder.get()

if (view != null) {
UiThreadUtil.runOnUiThread {
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(view.windowToken, 0)
if (!keepFocus) {
view.clearFocus()
fun maybeClearFocus() {
if (!keepFocus) {
view.clearFocus()
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !animated) {
controller.startControlRequest(view) { insetsController ->
insetsController.finish(false)

view.post {
maybeClearFocus()
}
}
} else {
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(view.windowToken, 0)

maybeClearFocus()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ class KeyboardControllerModule(
}

@ReactMethod
fun dismiss(keepFocus: Boolean) {
module.dismiss(keepFocus)
fun dismiss(
keepFocus: Boolean,
animated: Boolean,
) {
module.dismiss(keepFocus, animated)
}

@ReactMethod
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/api/keyboard-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ If you want to hide a keyboard and keep focus then you can pass `keepFocus` opti
await KeyboardController.dismiss({ keepFocus: true });
```

If you want to hide keyboard immediately (i. e. without animation), you can pass `animated` option:

```ts
await KeyboardController.dismiss({ animated: false });
```

:::info What is the difference comparing to `react-native` implementation?
The equivalent method from `react-native` relies on specific internal components, such as `TextInput`, and may not work as intended if a custom input component is used.

Expand Down
17 changes: 17 additions & 0 deletions e2e/kit/012-close-keyboard.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,21 @@ describe("`KeyboardController.dismiss()` specification", () => {
await waitAndTap("blur_from_ref");
await expect(element(by.id("input"))).not.toBeFocused();
});

it("should reveal a keyboard", async () => {
await waitAndTap("keep_focus_button");
await waitAndTap("input");
await waitForExpect(async () => {
await expectBitmapsToBeEqual("CloseKeyboardOpened");
});
});

it("should close keyboard immediately", async () => {
await waitAndTap("animated_button");
await waitAndTap("close_keyboard_button");
await expect(element(by.id("input"))).not.toBeFocused();
await waitForExpect(async () => {
await expectBitmapsToBeEqual("CloseKeyboardClosed");
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/kit/assets/android/e2e_emulator_28/CloseKeyboardOpened.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/kit/assets/android/e2e_emulator_31/CloseKeyboardOpened.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/kit/assets/ios/iPhone 13 Pro/CloseKeyboardOpened.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/kit/assets/ios/iPhone 14 Pro/CloseKeyboardOpened.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/kit/assets/ios/iPhone 15 Pro/CloseKeyboardOpened.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/kit/assets/ios/iPhone 16 Pro/CloseKeyboardOpened.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion example/src/screens/Examples/Close/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function CloseScreen() {

const ref = useRef<TextInput>(null);
const [keepFocus, setKeepFocus] = useState(false);
const [animated, setAnimated] = useState(true);

return (
<View>
Expand All @@ -18,6 +19,11 @@ function CloseScreen() {
title={keepFocus ? "Keep focus" : "Don't keep focus"}
onPress={() => setKeepFocus(!keepFocus)}
/>
<Button
testID="animated_button"
title={animated ? "Animated" : "Instant"}
onPress={() => setAnimated(!animated)}
/>
<Button
testID="set_focus_to_current"
title="KeyboardController.setFocusTo('current')"
Expand All @@ -36,7 +42,7 @@ function CloseScreen() {
<Button
testID="close_keyboard_button"
title="Close keyboard"
onPress={() => KeyboardController.dismiss({ keepFocus })}
onPress={() => KeyboardController.dismiss({ keepFocus, animated })}
/>
<TextInput
ref={ref}
Expand Down
6 changes: 3 additions & 3 deletions ios/KeyboardControllerModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ - (void)preload
}

#ifdef RCT_NEW_ARCH_ENABLED
- (void)dismiss:(BOOL)keepFocus
- (void)dismiss:(BOOL)keepFocus animated:(BOOL)animated
#else
RCT_EXPORT_METHOD(dismiss : (BOOL)keepFocus)
RCT_EXPORT_METHOD(dismiss : (BOOL)keepFocus animated : (BOOL)animated)
#endif
{
dispatch_async(dispatch_get_main_queue(), ^{
[KeyboardControllerModuleImpl dismiss:keepFocus];
[KeyboardControllerModuleImpl dismiss:keepFocus animated:animated];
});
}

Expand Down
42 changes: 26 additions & 16 deletions ios/KeyboardControllerModuleImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,36 @@ public class KeyboardControllerModuleImpl: NSObject {
private static let keyboardRevealGestureName = "keyboardRevealGesture"

@objc
public static func dismiss(_ keepFocus: Bool) {
let responder = UIResponder.current
public static func dismiss(_ keepFocus: Bool, animated: Bool) {
let work = {
let responder = UIResponder.current

if keepFocus {
guard let input = responder as? TextInput else { return }
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTextInputTapped(_:)))
tapGesture.name = keyboardRevealGestureName
input.addGestureRecognizer(tapGesture)
if keepFocus {
guard let input = responder as? TextInput else { return }
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onTextInputTapped(_:)))
tapGesture.name = keyboardRevealGestureName
input.addGestureRecognizer(tapGesture)

input.inputView = UIView()
input.reloadInputViews()
input.inputView = UIView()
input.reloadInputViews()

NotificationCenter.default.addObserver(
self,
selector: #selector(onResponderResigned(_:)),
name: UITextField.textDidEndEditingNotification,
object: input
)
NotificationCenter.default.addObserver(
self,
selector: #selector(onResponderResigned(_:)),
name: UITextField.textDidEndEditingNotification,
object: input
)
} else {
responder?.resignFirstResponder()
}
}

if !animated {
UIView.performWithoutAnimation {
work()
}
} else {
responder?.resignFirstResponder()
work()
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ KeyboardEvents.addListener("keyboardDidShow", (e) => {
lastState = e;
});

const dismiss = async (options?: DismissOptions): Promise<void> => {
const dismiss = async (options?: Partial<DismissOptions>): Promise<void> => {
const keepFocus = options?.keepFocus ?? false;
const animated = options?.animated ?? true;

return new Promise((resolve) => {
if (isClosed) {
Expand All @@ -41,7 +42,7 @@ const dismiss = async (options?: DismissOptions): Promise<void> => {
subscription.remove();
});

KeyboardControllerNative.dismiss(keepFocus);
KeyboardControllerNative.dismiss(keepFocus, animated);
});
};
const isVisible = () => !isClosed;
Expand Down
2 changes: 1 addition & 1 deletion src/specs/NativeKeyboardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface Spec extends TurboModule {
setInputMode(mode: number): void;
setDefaultMode(): void;
preload(): void;
dismiss(keepFocus: boolean): void;
dismiss(keepFocus: boolean, animated: boolean): void;
setFocusTo(direction: string): void;

// event emitter
Expand Down
6 changes: 5 additions & 1 deletion src/types/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export type DismissOptions = {
* A boolean property indicating whether focus should be kept on the input after dismissing the keyboard. Default is `false`.
*/
keepFocus: boolean;
/**
* A boolean property controlling whether dismissal should be animated. Default is `true`.
*/
animated: boolean;
};
export type KeyboardControllerModule = {
// android only
Expand Down Expand Up @@ -111,7 +115,7 @@ export type KeyboardControllerNativeModule = {
// ios only
preload: () => void;
// all platforms
dismiss: (keepFocus: boolean) => void;
dismiss: (keepFocus: boolean, animated: boolean) => void;
setFocusTo: (direction: Direction) => void;
// native event module stuff
addListener: (eventName: string) => void;
Expand Down
Loading