Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dismiss keyboard #306

Merged
merged 4 commits into from
Dec 23, 2023
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
25 changes: 25 additions & 0 deletions FabricExample/__tests__/close-keyboard.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Button } from 'react-native';
import { fireEvent, render } from '@testing-library/react-native';

import { KeyboardController } from 'react-native-keyboard-controller';

function CloseKeyboard() {
return (
<Button
title='Close keyboard'
testID='close_keyboard'
onPress={() => KeyboardController.dismiss()}
/>
);
}

describe('closing keyboard flow', () => {
it('should have a mock version of `KeyboardController.dismiss`', () => {
const { getByTestId } = render(<CloseKeyboard />);

fireEvent.press(getByTestId('close_keyboard'));

expect(KeyboardController.dismiss).toBeCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions FabricExample/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export enum ScreenNames {
NATIVE_STACK = 'NATIVE_STACK',
KEYBOARD_AVOIDING_VIEW = 'KEYBOARD_AVOIDING_VIEW',
ENABLED_DISABLED = 'ENABLED_DISABLED',
CLOSE = 'CLOSE',
}
10 changes: 10 additions & 0 deletions FabricExample/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import NativeStack from '../NestedStack';
import KeyboardAvoidingViewExample from '../../screens/Examples/KeyboardAvoidingView';
import EnabledDisabled from '../../screens/Examples/EnabledDisabled';
import AwareScrollViewStickyFooter from '../../screens/Examples/AwareScrollViewStickyFooter';
import CloseScreen from '../../screens/Examples/Close';

export type ExamplesStackParamList = {
[ScreenNames.ANIMATED_EXAMPLE]: undefined;
Expand All @@ -31,6 +32,7 @@ export type ExamplesStackParamList = {
[ScreenNames.NATIVE_STACK]: undefined;
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: undefined;
[ScreenNames.ENABLED_DISABLED]: undefined;
[ScreenNames.CLOSE]: undefined;
};

const Stack = createStackNavigator<ExamplesStackParamList>();
Expand Down Expand Up @@ -76,6 +78,9 @@ const options = {
[ScreenNames.ENABLED_DISABLED]: {
title: 'Enabled/disabled',
},
[ScreenNames.CLOSE]: {
title: 'Close keyboard',
},
};

const ExamplesStack = () => (
Expand Down Expand Up @@ -145,6 +150,11 @@ const ExamplesStack = () => (
component={EnabledDisabled}
options={options[ScreenNames.ENABLED_DISABLED]}
/>
<Stack.Screen
name={ScreenNames.CLOSE}
component={CloseScreen}
options={options[ScreenNames.CLOSE]}
/>
</Stack.Navigator>
);

Expand Down
30 changes: 30 additions & 0 deletions FabricExample/src/screens/Examples/Close/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button, StyleSheet, TextInput, View } from "react-native";
import { KeyboardController } from "react-native-keyboard-controller";

function CloseScreen() {
return (
<View>
<Button
title="Close keyboard"
onPress={() => KeyboardController.dismiss()}
testID="close_keyboard_button"
/>
<TextInput style={styles.input} placeholder="Touch to open the keyboard..." placeholderTextColor="#7C7C7C" />
</View>
);
};

const styles = StyleSheet.create({
input: {
height: 50,
width: "84%",
borderWidth: 2,
borderColor: "#3C3C3C",
borderRadius: 8,
alignSelf: 'center',
paddingHorizontal: 8,
marginTop: 16,
},
});

export default CloseScreen;
6 changes: 6 additions & 0 deletions FabricExample/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,10 @@ export const examples: Example[] = [
info: ScreenNames.ENABLED_DISABLED,
icons: '💡',
},
{
title: 'Close',
testID: 'close',
info: ScreenNames.CLOSE,
icons: '❌',
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class KeyboardControllerModule(mReactContext: ReactApplicationContext) : NativeK
module.setDefaultMode()
}

override fun dismiss() {
module.dismiss()
}

override fun addListener(eventName: String?) {
/* Required for RN built-in Event Emitter Calls. */
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.reactnativekeyboardcontroller.modules

import android.content.Context
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

Expand All @@ -15,6 +18,16 @@ class KeyboardControllerModuleImpl(private val mReactContext: ReactApplicationCo
setSoftInputMode(mDefaultMode)
}

fun dismiss() {
val activity = mReactContext.currentActivity
val view: View? = activity?.currentFocus

if (view != null) {
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.hideSoftInputFromWindow(view.windowToken, 0)
}
}

private fun setSoftInputMode(mode: Int) {
UiThreadUtil.runOnUiThread {
if (getCurrentMode() != mode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class KeyboardControllerModule(mReactContext: ReactApplicationContext) : ReactCo
module.setDefaultMode()
}

@ReactMethod
fun dismiss() {
module.dismiss()
}

@Suppress("detekt:UnusedParameter")
@ReactMethod
fun addListener(eventName: String?) {
Expand Down
48 changes: 29 additions & 19 deletions docs/docs/api/keyboard-controller.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
---
sidebar_position: 5
keywords: [react-native-keyboard-controller, KeyboardController, module, windowSoftInputMode, adjustResize, adjustPan]
keywords: [react-native-keyboard-controller, react-native, KeyboardController, module, dismiss, dismiss keyboard, windowSoftInputMode, adjustResize, adjustPan]
---

# KeyboardController

`KeyboardController` is an object which has two functions:
The `KeyboardController` module in React Native provides a convenient set of methods for managing the behavior of the keyboard. With seamless runtime adjustments, this module allows developers to dynamically change the `windowSoftInputMode` on Android and dismiss the keyboard on both platforms.

- `setInputMode` - used to change `windowSoftInputMode` in runtime;
- `setDefaultMode` - used to restore default `windowSoftInputMode` (which is declared in `AndroidManifest.xml`);
## Methods

## Example
### `setInputMode`

This method is used to dynamically change the `windowSoftInputMode` during runtime in an Android application. It takes an argument that specifies the desired input mode. The example provided sets the input mode to `SOFT_INPUT_ADJUST_RESIZE`:

```ts
import {
KeyboardController,
AndroidSoftInputModes,
} from "react-native-keyboard-controller";

export const useResizeMode = () => {
useEffect(() => {
KeyboardController.setInputMode(
AndroidSoftInputModes.SOFT_INPUT_ADJUST_RESIZE
);

return () => KeyboardController.setDefaultMode();
}, []);
};
KeyboardController.setInputMode(AndroidSoftInputModes.SOFT_INPUT_ADJUST_RESIZE);
```

### `setDefaultMode`

This method is used to restore the default `windowSoftInputMode` declared in the `AndroidManifest.xml`. It resets the input mode to the default value:

```ts
KeyboardController.setDefaultMode();
```

### `dismiss`

This method is used to hide the keyboard. It triggers the dismissal of the keyboard:

```ts
KeyboardController.dismiss();
```

:::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.

In contrast, the described method enables keyboard dismissal for any focused input, extending functionality beyond the limitations of the default implementation.
:::
25 changes: 25 additions & 0 deletions example/__tests__/close-keyboard.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Button } from 'react-native';
import { fireEvent, render } from '@testing-library/react-native';

import { KeyboardController } from 'react-native-keyboard-controller';

function CloseKeyboard() {
return (
<Button
title='Close keyboard'
testID='close_keyboard'
onPress={() => KeyboardController.dismiss()}
/>
);
}

describe('closing keyboard flow', () => {
it('should have a mock version of `KeyboardController.dismiss`', () => {
const { getByTestId } = render(<CloseKeyboard />);

fireEvent.press(getByTestId('close_keyboard'));

expect(KeyboardController.dismiss).toBeCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions example/src/constants/screenNames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export enum ScreenNames {
NATIVE_STACK = 'NATIVE_STACK',
KEYBOARD_AVOIDING_VIEW = 'KEYBOARD_AVOIDING_VIEW',
ENABLED_DISABLED = 'ENABLED_DISABLED',
CLOSE = 'CLOSE',
}
10 changes: 10 additions & 0 deletions example/src/navigation/ExamplesStack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import KeyboardAvoidingViewExample from '../../screens/Examples/KeyboardAvoiding
import ReanimatedChatFlatlist from '../../screens/Examples/ReanimatedChatFlatlist';
import EnabledDisabled from '../../screens/Examples/EnabledDisabled';
import AwareScrollViewStickyFooter from '../../screens/Examples/AwareScrollViewStickyFooter';
import CloseScreen from '../../screens/Examples/Close';

export type ExamplesStackParamList = {
[ScreenNames.ANIMATED_EXAMPLE]: undefined;
Expand All @@ -32,6 +33,7 @@ export type ExamplesStackParamList = {
[ScreenNames.NATIVE_STACK]: undefined;
[ScreenNames.KEYBOARD_AVOIDING_VIEW]: undefined;
[ScreenNames.ENABLED_DISABLED]: undefined;
[ScreenNames.CLOSE]: undefined;
};

const Stack = createStackNavigator<ExamplesStackParamList>();
Expand Down Expand Up @@ -80,6 +82,9 @@ const options = {
[ScreenNames.ENABLED_DISABLED]: {
title: 'Enabled/disabled',
},
[ScreenNames.CLOSE]: {
title: 'Close keyboard',
},
};

const ExamplesStack = () => (
Expand Down Expand Up @@ -154,6 +159,11 @@ const ExamplesStack = () => (
component={EnabledDisabled}
options={options[ScreenNames.ENABLED_DISABLED]}
/>
<Stack.Screen
name={ScreenNames.CLOSE}
component={CloseScreen}
options={options[ScreenNames.CLOSE]}
/>
</Stack.Navigator>
);

Expand Down
30 changes: 30 additions & 0 deletions example/src/screens/Examples/Close/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Button, StyleSheet, TextInput, View } from "react-native";
import { KeyboardController } from "react-native-keyboard-controller";

function CloseScreen() {
return (
<View>
<Button
title="Close keyboard"
onPress={() => KeyboardController.dismiss()}
testID="close_keyboard_button"
/>
<TextInput style={styles.input} placeholder="Touch to open the keyboard..." placeholderTextColor="#7C7C7C" />
</View>
);
};

const styles = StyleSheet.create({
input: {
height: 50,
width: "84%",
borderWidth: 2,
borderColor: "#3C3C3C",
borderRadius: 8,
alignSelf: 'center',
paddingHorizontal: 8,
marginTop: 16,
},
});

export default CloseScreen;
6 changes: 6 additions & 0 deletions example/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,10 @@ export const examples: Example[] = [
info: ScreenNames.ENABLED_DISABLED,
icons: '💡',
},
{
title: 'Close',
testID: 'close',
info: ScreenNames.CLOSE,
icons: '❌',
},
];
14 changes: 14 additions & 0 deletions ios/KeyboardControllerModule.mm
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ - (void)setInputMode:(double)mode
{
}

#ifdef RCT_NEW_ARCH_ENABLED
- (void)dismiss
#else
RCT_EXPORT_METHOD(dismiss)
#endif
{
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder)
to:nil
from:nil
forEvent:nil];
});
}

+ (KeyboardController *)shared
{
return shared;
Expand Down
1 change: 1 addition & 0 deletions jest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const mock = {
KeyboardController: {
setInputMode: jest.fn(),
setDefaultMode: jest.fn(),
dismiss: jest.fn(),
},
KeyboardEvents: {
addListener: jest.fn(() => ({ remove: jest.fn() })),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"react-native",
"keyboard",
"animation",
"dismiss",
"focused input",
"text changed",
"avoiding view",
Expand Down
1 change: 1 addition & 0 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const NOOP = () => {};
export const KeyboardController: KeyboardControllerModule = {
setDefaultMode: NOOP,
setInputMode: NOOP,
dismiss: NOOP,
addListener: NOOP,
removeListeners: NOOP,
};
Expand Down
1 change: 1 addition & 0 deletions src/specs/NativeKeyboardController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Spec extends TurboModule {
// methods
setInputMode(mode: number): void;
setDefaultMode(): void;
dismiss(): void;

// event emitter
addListener: (eventName: string) => void;
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export type KeyboardControllerModule = {
// android only
setDefaultMode: () => void;
setInputMode: (mode: number) => void;
// all platforms
dismiss: () => void;
// native event module stuff
addListener: (eventName: string) => void;
removeListeners: (count: number) => void;
Expand Down