Skip to content

Commit

Permalink
feat: async dismiss (#693)
Browse files Browse the repository at this point in the history
## 📜 Description

Make `dismiss` method async to get rid off boilerplate/utilities
functions in main project.

## 💡 Motivation and Context

Originally this idea was suggested by @terrysahaidak. Before many
projects had its own implementation of this method. But @terrysahaidak
suggested and I agreed that it would be better to keep this method
directly inside the library.

## 📢 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 -->

### Docs

- include methods signature in methods description;

### JS

- changed mock implementation;
- make `dismiss` method async;
- create `module` file;
- set global listeners for `KeyboardEvents` inside `module` file;

## 🤔 How Has This Been Tested?

Tested on CI.

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
  • Loading branch information
kirillzyusko authored Nov 18, 2024
1 parent 8ba5bb4 commit 80d8972
Show file tree
Hide file tree
Showing 11 changed files with 82 additions and 40 deletions.
15 changes: 4 additions & 11 deletions FabricExample/src/screens/Examples/Toolbar/Contacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import {
TouchableOpacity,
View,
} from "react-native";
import {
KeyboardController,
KeyboardEvents,
} from "react-native-keyboard-controller";
import { KeyboardController } from "react-native-keyboard-controller";

export type Contact = {
image: string;
Expand Down Expand Up @@ -45,13 +42,9 @@ type Props = {

const AutoFillContacts = ({ onContactSelected }: Props) => {
const [visible, setVisible] = useState(false);
const handlePresentModalPress = useCallback(() => {
const subscription = KeyboardEvents.addListener("keyboardDidHide", () => {
setVisible(true);
subscription.remove();
});

KeyboardController.dismiss();
const handlePresentModalPress = useCallback(async () => {
await KeyboardController.dismiss();
setVisible(true);
}, []);

const handleCloseModalPress = useCallback(() => {
Expand Down
22 changes: 19 additions & 3 deletions docs/docs/api/keyboard-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ The `KeyboardController` module in React Native provides a convenient set of met

### `setInputMode` <div className="label android"></div>

```ts
static setInputMode(mode: AndroidSoftInputModes): void;
```

This method is used to dynamically change the `windowSoftInputMode` (`softwareKeyboardLayoutMode` in Expo terminology) 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
Expand All @@ -38,6 +42,10 @@ A combination of `adjustResize` + `edge-to-edge` mode will result in behavior si

### `setDefaultMode` <div className="label android"></div>

```ts
static setDefaultMode(): void;
```

This method is used to restore the default `windowSoftInputMode` (`softwareKeyboardLayoutMode` in Expo terminology) declared in the `AndroidManifest.xml` (or `app.json` in Expo case). It resets the input mode to the default value:

```ts
Expand All @@ -46,10 +54,14 @@ KeyboardController.setDefaultMode();

### `dismiss`

This method is used to hide the keyboard. It triggers the dismissal of the keyboard:
```ts
static dismiss(): Promise<void>;
```

This method is used to hide the keyboard. It triggers the dismissal of the keyboard. The method returns promise that will be resolved only when keyboard is fully hidden (if keyboard is already hidden it will resolve immediately):

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

:::info What is the difference comparing to `react-native` implementation?
Expand All @@ -60,12 +72,16 @@ In contrast, the described method enables keyboard dismissal for any focused inp

### `setFocusTo`

```ts
static setFocusTo(direction: "prev" | "current" | "next"): void;
```

This method sets focus to the selected field. Possible values:

- `prev` - set focus to the previous field;
- `current` - set focus to the last focused field (i. e. if keyboard was closed you can restore focus);
- `next` - set focus to the next field.

```ts
setFocusTo("next");
KeyboardController.setFocusTo("next");
```
15 changes: 4 additions & 11 deletions example/src/screens/Examples/Toolbar/Contacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import {
TouchableOpacity,
View,
} from "react-native";
import {
KeyboardController,
KeyboardEvents,
} from "react-native-keyboard-controller";
import { KeyboardController } from "react-native-keyboard-controller";

export type Contact = {
image: string;
Expand Down Expand Up @@ -45,13 +42,9 @@ type Props = {

const AutoFillContacts = ({ onContactSelected }: Props) => {
const [visible, setVisible] = useState(false);
const handlePresentModalPress = useCallback(() => {
const subscription = KeyboardEvents.addListener("keyboardDidHide", () => {
setVisible(true);
subscription.remove();
});

KeyboardController.dismiss();
const handlePresentModalPress = useCallback(async () => {
await KeyboardController.dismiss();
setVisible(true);
}, []);

const handleCloseModalPress = useCallback(() => {
Expand Down
2 changes: 1 addition & 1 deletion jest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const mock = {
KeyboardController: {
setInputMode: jest.fn(),
setDefaultMode: jest.fn(),
dismiss: jest.fn(),
dismiss: jest.fn().mockReturnValue(Promise.resolve()),
setFocusTo: jest.fn(),
},
AndroidSoftInputModes: {
Expand Down
9 changes: 1 addition & 8 deletions src/bindings.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { NativeEventEmitter, Platform } from "react-native";

import type {
FocusedInputEventsModule,
KeyboardControllerModule,
KeyboardControllerNativeModule,
KeyboardControllerProps,
KeyboardEventsModule,
Expand Down Expand Up @@ -40,13 +39,7 @@ export const KeyboardEvents: KeyboardEventsModule = {
addListener: (name, cb) =>
eventEmitter.addListener(KEYBOARD_CONTROLLER_NAMESPACE + name, cb),
};
export const KeyboardController: KeyboardControllerModule = {
setDefaultMode: KeyboardControllerNative.setDefaultMode,
setInputMode: KeyboardControllerNative.setInputMode,
setFocusTo: KeyboardControllerNative.setFocusTo,
// additional function is needed because of this https://github.com/kirillzyusko/react-native-keyboard-controller/issues/684
dismiss: () => KeyboardControllerNative.dismiss(),
};

/**
* This API is not documented, it's for internal usage only (for now), and is a subject to potential breaking changes in future.
* Use it with cautious.
Expand Down
6 changes: 4 additions & 2 deletions src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { View } from "react-native";

import type {
FocusedInputEventsModule,
KeyboardControllerModule,
KeyboardControllerNativeModule,
KeyboardControllerProps,
KeyboardEventsModule,
KeyboardGestureAreaProps,
Expand All @@ -13,11 +13,13 @@ import type { EmitterSubscription } from "react-native";

const NOOP = () => {};

export const KeyboardController: KeyboardControllerModule = {
export const KeyboardControllerNative: KeyboardControllerNativeModule = {
setDefaultMode: NOOP,
setInputMode: NOOP,
dismiss: NOOP,
setFocusTo: NOOP,
addListener: NOOP,
removeListeners: NOOP,
};
export const KeyboardEvents: KeyboardEventsModule = {
addListener: () => ({ remove: NOOP } as EmitterSubscription),
Expand Down
3 changes: 2 additions & 1 deletion src/components/KeyboardToolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { StyleSheet, Text, View } from "react-native";

import { FocusedInputEvents, KeyboardController } from "../../bindings";
import { FocusedInputEvents } from "../../bindings";
import { KeyboardController } from "../../module";
import useColorScheme from "../hooks/useColorScheme";
import KeyboardStickyView from "../KeyboardStickyView";

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect, useLayoutEffect } from "react";

import { KeyboardController } from "../bindings";
import { AndroidSoftInputModes } from "../constants";
import { useKeyboardContext } from "../context";
import { KeyboardController } from "../module";

import type { AnimatedContext, ReanimatedContext } from "../context";
import type { FocusedInputHandler, KeyboardHandler } from "../types";
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./animated";
export * from "./context";
export * from "./hooks";
export * from "./constants";
export * from "./module";
export * from "./types";

export {
Expand Down
37 changes: 37 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { KeyboardControllerNative, KeyboardEvents } from "./bindings";

import type { KeyboardControllerModule } from "./types";

let isVisible = false;

KeyboardEvents.addListener("keyboardDidHide", () => {
isVisible = false;
});

KeyboardEvents.addListener("keyboardDidShow", () => {
isVisible = true;
});

const dismiss = async (): Promise<void> => {
return new Promise((resolve) => {
if (!isVisible) {
resolve();

return;
}

const subscription = KeyboardEvents.addListener("keyboardDidHide", () => {
resolve(undefined);
subscription.remove();
});

KeyboardControllerNative.dismiss();
});
};

export const KeyboardController: KeyboardControllerModule = {
setDefaultMode: KeyboardControllerNative.setDefaultMode,
setInputMode: KeyboardControllerNative.setInputMode,
setFocusTo: KeyboardControllerNative.setFocusTo,
dismiss: dismiss,
};
10 changes: 8 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,20 @@ export type KeyboardControllerModule = {
setDefaultMode: () => void;
setInputMode: (mode: number) => void;
// all platforms
dismiss: () => void;
dismiss: () => Promise<void>;
setFocusTo: (direction: Direction) => void;
};
export type KeyboardControllerNativeModule = {
// android only
setDefaultMode: () => void;
setInputMode: (mode: number) => void;
// all platforms
dismiss: () => void;
setFocusTo: (direction: Direction) => void;
// native event module stuff
addListener: (eventName: string) => void;
removeListeners: (count: number) => void;
} & KeyboardControllerModule;
};

// Event module declarations
export type KeyboardControllerEvents =
Expand Down

0 comments on commit 80d8972

Please sign in to comment.