Skip to content

Commit 692494f

Browse files
authored
feat: dismiss without animation (#1127)
## 📜 Description Added ability to hide keyboard instantly. ## 💡 Motivation and Context This PR has been inspired by this post in twitter: https://x.com/klemensstrasser/status/1955284572825853957?t=Rui36_sZCd3pEoKFc02N9w&s=35 In this particular transition it makes sense to instantly hide the keyboard, so that main shared transition look consistent and additional animations don't distract the attention. So in this PR I'm adding a new option to `dismiss` method: `animated`. You can specify `false` and then keyboard will be dismissed instantly. By default this option is `true`, but when you develop complex shared animation transitions it may make sense to hide keyboard immediately 👀 The other reason why I am adding this is because I've seen in some projects (Expensify, for example) that keyboard **may** be hidden instantly in some edge cases (that was a reminder that you need to use `onEnd` for sync last animation frame). Now this is not the edge case, but something that we can control as well - so it will be easier to test various components, such as `KeyboardAvoidingView`/`KeyboardAwareScrollView` to assure they work consistently with immediate close too. ## 📢 Changelog <!-- High level overview of important changes --> <!-- For example: fixed status bar manipulation; added new types declarations; --> <!-- If your changes don't affect one of platform/language below - then remove this platform/language --> ### E2E - added new test for testing instant hide; - updated assets for close functionality; ### Docs - added a note that you can hide keyboard without animation; ### JS - added `animated` prop to `types.ts`. ### iOS - hide keyboard using `performWithoutAnimation` if `animated` is `true`. ### Android - hide keyboard using `WindowInsetsController` if `animated` is `true`. ## 🤔 How Has This Been Tested? Tested on: - Pixel 7 Pro (Android 16); - iPhone 16 Pro (iOS 18). ## 📸 Screenshots (if appropriate): |Android|iOS| |--------|---| |<video src="https://github.com/user-attachments/assets/73ae03a3-d69b-469a-80de-0b21852fcf2c">|<video src="https://github.com/user-attachments/assets/0590365c-d6d7-46ce-8458-2855a227c5a9">| ## 📝 Checklist - [x] CI successfully passed - [x] I added new mocks and corresponding unit-tests if library API was changed
1 parent 8138c60 commit 692494f

30 files changed

+110
-34
lines changed

FabricExample/src/screens/Examples/Close/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function CloseScreen() {
1010

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

1415
return (
1516
<View>
@@ -18,6 +19,11 @@ function CloseScreen() {
1819
title={keepFocus ? "Keep focus" : "Don't keep focus"}
1920
onPress={() => setKeepFocus(!keepFocus)}
2021
/>
22+
<Button
23+
testID="animated_button"
24+
title={animated ? "Animated" : "Instant"}
25+
onPress={() => setAnimated(!animated)}
26+
/>
2127
<Button
2228
testID="set_focus_to_current"
2329
title="KeyboardController.setFocusTo('current')"
@@ -36,7 +42,7 @@ function CloseScreen() {
3642
<Button
3743
testID="close_keyboard_button"
3844
title="Close keyboard"
39-
onPress={() => KeyboardController.dismiss({ keepFocus })}
45+
onPress={() => KeyboardController.dismiss({ keepFocus, animated })}
4046
/>
4147
<TextInput
4248
ref={ref}

android/src/fabric/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ class KeyboardControllerModule(
2222
module.preload()
2323
}
2424

25-
override fun dismiss(keepFocus: Boolean) {
26-
module.dismiss(keepFocus)
25+
override fun dismiss(
26+
keepFocus: Boolean,
27+
animated: Boolean,
28+
) {
29+
module.dismiss(keepFocus, animated)
2730
}
2831

2932
override fun setFocusTo(direction: String) {

android/src/main/java/com/reactnativekeyboardcontroller/modules/KeyboardControllerModuleImpl.kt

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package com.reactnativekeyboardcontroller.modules
22

33
import android.content.Context
4+
import android.os.Build
45
import android.view.View
56
import android.view.WindowManager
67
import android.view.inputmethod.InputMethodManager
78
import com.facebook.react.bridge.ReactApplicationContext
89
import com.facebook.react.bridge.UiThreadUtil
10+
import com.reactnativekeyboardcontroller.interactive.KeyboardAnimationController
911
import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder
1012
import com.reactnativekeyboardcontroller.traversal.ViewHierarchyNavigator
1113

1214
class KeyboardControllerModuleImpl(
1315
private val mReactContext: ReactApplicationContext,
1416
) {
17+
private val controller = KeyboardAnimationController()
1518
private val mDefaultMode: Int = getCurrentMode()
1619

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

29-
fun dismiss(keepFocus: Boolean) {
32+
fun dismiss(
33+
keepFocus: Boolean,
34+
animated: Boolean,
35+
) {
3036
val activity = mReactContext.currentActivity
3137
val view: View? = FocusedInputHolder.get()
3238

3339
if (view != null) {
3440
UiThreadUtil.runOnUiThread {
35-
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
36-
imm?.hideSoftInputFromWindow(view.windowToken, 0)
37-
if (!keepFocus) {
38-
view.clearFocus()
41+
fun maybeClearFocus() {
42+
if (!keepFocus) {
43+
view.clearFocus()
44+
}
45+
}
46+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !animated) {
47+
controller.startControlRequest(view) { insetsController ->
48+
insetsController.finish(false)
49+
50+
view.post {
51+
maybeClearFocus()
52+
}
53+
}
54+
} else {
55+
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
56+
imm?.hideSoftInputFromWindow(view.windowToken, 0)
57+
58+
maybeClearFocus()
3959
}
4060
}
4161
}

android/src/paper/java/com/reactnativekeyboardcontroller/KeyboardControllerModule.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ class KeyboardControllerModule(
2828
}
2929

3030
@ReactMethod
31-
fun dismiss(keepFocus: Boolean) {
32-
module.dismiss(keepFocus)
31+
fun dismiss(
32+
keepFocus: Boolean,
33+
animated: Boolean,
34+
) {
35+
module.dismiss(keepFocus, animated)
3336
}
3437

3538
@ReactMethod

docs/docs/api/keyboard-controller.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ If you want to hide a keyboard and keep focus then you can pass `keepFocus` opti
8282
await KeyboardController.dismiss({ keepFocus: true });
8383
```
8484

85+
If you want to hide keyboard immediately (i. e. without animation), you can pass `animated` option:
86+
87+
```ts
88+
await KeyboardController.dismiss({ animated: false });
89+
```
90+
8591
:::info What is the difference comparing to `react-native` implementation?
8692
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.
8793

e2e/kit/012-close-keyboard.e2e.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,21 @@ describe("`KeyboardController.dismiss()` specification", () => {
6161
await waitAndTap("blur_from_ref");
6262
await expect(element(by.id("input"))).not.toBeFocused();
6363
});
64+
65+
it("should reveal a keyboard", async () => {
66+
await waitAndTap("keep_focus_button");
67+
await waitAndTap("input");
68+
await waitForExpect(async () => {
69+
await expectBitmapsToBeEqual("CloseKeyboardOpened");
70+
});
71+
});
72+
73+
it("should close keyboard immediately", async () => {
74+
await waitAndTap("animated_button");
75+
await waitAndTap("close_keyboard_button");
76+
await expect(element(by.id("input"))).not.toBeFocused();
77+
await waitForExpect(async () => {
78+
await expectBitmapsToBeEqual("CloseKeyboardClosed");
79+
});
80+
});
6481
});
65.5 KB
Loading
2.66 KB
Loading
2.71 KB
Loading
71.9 KB
Loading

0 commit comments

Comments
 (0)