Skip to content

Commit

Permalink
feat: KeyboardToolbar callbacks (#419)
Browse files Browse the repository at this point in the history
## 馃摐 Description

Added `onDoneCallback`/`onNextCallback`/`onPrevCallback` functions to
`KeyboardToolbar`.

## 馃挕 Motivation and Context

It's often needed to customize behavior when buttons are pressed, for
example do a haptic feedback or even play a sound.

Before this PR it wasn't possible to add an action that will be called
whenever user presses a particular button along with the default action.
But in this PR I added it.

Initially I wanted to provide `onNextPress` functions that would
redefine an entire handler. But in this case users will have to go into
source code and understand how `KeyboardToolbar` works under the hood.
Even though it's not difficult it's better to keep such things
internally (and in most cases users will rely on defult behavior - in
case if we need to re-define a behavior I'll add new functions later).

## 馃摙 Changelog

### Docs

- added docs about new
`onDoneCallback`/`onNextCallback`/`onPrevCallback` callbacks;

### JS

- added `onDoneCallback`/`onNextCallback`/`onPrevCallback`;

## 馃 How Has This Been Tested?

Tested on Pixel 7 Pro (android 14) and iPhone 11 (iOS 17.4).

## 馃摳 Screenshots (if appropriate):

There is no way to test haptic visually 馃榾 

## 馃摑 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
  • Loading branch information
kirillzyusko committed Apr 27, 2024
1 parent 64b2e21 commit 7d2ccb5
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 7 deletions.
13 changes: 12 additions & 1 deletion FabricExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,13 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- RNReactNativeHapticFeedback (2.2.0):
- RCT-Folly
- RCTRequired
- RCTTypeSafety
- React-Codegen
- React-Core
- ReactCommon/turbomodule/core
- RNReanimated (3.8.0):
- glog
- hermes-engine
Expand Down Expand Up @@ -1415,6 +1422,7 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
Expand Down Expand Up @@ -1548,6 +1556,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-masked-view/masked-view"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
Expand All @@ -1558,7 +1568,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953
FBLazyVector: f64d1e2ea739b4d8f7e4740cde18089cd97fe864
Flipper: c7a0093234c4bdd456e363f2f19b2e4b27652d44
Flipper-Boost-iOSX: fd1e2b8cbef7e662a122412d7ac5f5bea715403c
Expand Down Expand Up @@ -1624,6 +1634,7 @@ SPEC CHECKSUMS:
ReactCommon: 2aa35648354bd4c4665b9a5084a7d37097b89c10
RNCMaskedView: 090213d32d8b3bb83a4dcb7d12c18f0152591906
RNGestureHandler: 0639a230f5bfdf0ffb2eae5dbc3c5f664d3f14ab
RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37
RNReanimated: 928a0e9d20b70bfc57ae741560ef3635937d3e9d
RNScreens: 9cd50a78d3723dfa55252a2220a94a7188deb180
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Expand Down
1 change: 1 addition & 0 deletions FabricExample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"react": "18.2.0",
"react-native": "0.73.6",
"react-native-gesture-handler": "2.15.0",
"react-native-haptic-feedback": "^2.2.0",
"react-native-keyboard-controller": "link:../",
"react-native-reanimated": "3.8.0",
"react-native-safe-area-context": "^4.9.0",
Expand Down
14 changes: 13 additions & 1 deletion FabricExample/src/screens/Examples/Toolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from "react";
import { StyleSheet, View } from "react-native";
import { Platform, StyleSheet, View } from "react-native";
import { trigger } from "react-native-haptic-feedback";
import {
KeyboardAwareScrollView,
KeyboardToolbar,
Expand All @@ -11,6 +12,14 @@ import AutoFillContacts from "./Contacts";

import type { Contact } from "./Contacts";

// Optional configuration
const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
};
const haptic = () =>
trigger(Platform.OS === "ios" ? "impactLight" : "keyboardTap", options);

export default function ToolbarExample() {
const [showAutoFill, setShowAutoFill] = useState(false);
const [name, setName] = useState("");
Expand Down Expand Up @@ -145,6 +154,9 @@ export default function ToolbarExample() {
<AutoFillContacts onContactSelected={onContactSelected} />
) : null
}
onDoneCallback={haptic}
onPrevCallback={haptic}
onNextCallback={haptic}
/>
</>
);
Expand Down
5 changes: 5 additions & 0 deletions FabricExample/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4599,6 +4599,11 @@ react-native-gesture-handler@2.15.0:
lodash "^4.17.21"
prop-types "^15.7.2"

react-native-haptic-feedback@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.2.0.tgz#bc46edd1f053265bfbe6c32487cbce074e099429"
integrity sha512-3tqJOjCguWhIrX0nkURn4yw6kXdsSDjjrvZCRjKXYGlL28hdQmoW2okAHduDTD9FWj9lA+lHgwFWgGs4aFNN7A==

"react-native-keyboard-controller@link:..":
version "0.0.0"
uid ""
Expand Down
63 changes: 63 additions & 0 deletions docs/docs/api/components/keyboard-toolbar/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,69 @@ const Icon: KeyboardToolbarProps["icon"] = ({ type }) => {
<KeyboardToolbar icon={Icon} />;
```

### `onDoneCallback`

A callback that is called when the user presses the **done** button along with the default action.

```tsx
import { Platform } from "react-native";
import { KeyboardToolbar } from "react-native-keyboard-controller";
import { trigger } from "react-native-haptic-feedback";

const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
};
const haptic = () =>
trigger(Platform.OS === "ios" ? "impactLight" : "keyboardTap", options);

// ...

<KeyboardToolbar onDoneCallback={haptic} />;
```

### `onNextCallback`

A callback that is called when the user presses the **next** button along with the default action.

```tsx
import { Platform } from "react-native";
import { KeyboardToolbar } from "react-native-keyboard-controller";
import { trigger } from "react-native-haptic-feedback";

const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
};
const haptic = () =>
trigger(Platform.OS === "ios" ? "impactLight" : "keyboardTap", options);

// ...

<KeyboardToolbar onNextCallback={haptic} />;
```

### `onPrevCallback`

A callback that is called when the user presses the **previous** button along with the default action.

```tsx
import { Platform } from "react-native";
import { KeyboardToolbar } from "react-native-keyboard-controller";
import { trigger } from "react-native-haptic-feedback";

const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
};
const haptic = () =>
trigger(Platform.OS === "ios" ? "impactLight" : "keyboardTap", options);

// ...

<KeyboardToolbar onPrevCallback={haptic} />;
```

### `showArrows`

A boolean prop indicating whether to show `next` and `prev` buttons. Can be useful to set it to `false` if you have only one input and want to show only `Done` button. Default to `true`.
Expand Down
6 changes: 6 additions & 0 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,8 @@ PODS:
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- RNReactNativeHapticFeedback (2.2.0):
- React-Core
- RNReanimated (3.8.0):
- glog
- RCT-Folly (= 2022.05.16.00)
Expand Down Expand Up @@ -1225,6 +1227,7 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
Expand Down Expand Up @@ -1355,6 +1358,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-masked-view/masked-view"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
RNReanimated:
:path: "../node_modules/react-native-reanimated"
RNScreens:
Expand Down Expand Up @@ -1430,6 +1435,7 @@ SPEC CHECKSUMS:
ReactCommon: 2aa35648354bd4c4665b9a5084a7d37097b89c10
RNCMaskedView: 090213d32d8b3bb83a4dcb7d12c18f0152591906
RNGestureHandler: 67fb54b3e6ca338a8044e85cd6f340265aa41091
RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9
RNReanimated: 00ee495a70897aa9d541e76debec14253133b812
RNScreens: 17e2f657f1b09a71ec3c821368a04acbb7ebcb46
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"react": "18.2.0",
"react-native": "0.73.6",
"react-native-gesture-handler": "2.15.0",
"react-native-haptic-feedback": "^2.2.0",
"react-native-keyboard-controller": "link:../",
"react-native-reanimated": "3.8.0",
"react-native-safe-area-context": "^4.9.0",
Expand Down
14 changes: 13 additions & 1 deletion example/src/screens/Examples/Toolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from "react";
import { StyleSheet, View } from "react-native";
import { Platform, StyleSheet, View } from "react-native";
import { trigger } from "react-native-haptic-feedback";
import {
KeyboardAwareScrollView,
KeyboardToolbar,
Expand All @@ -11,6 +12,14 @@ import AutoFillContacts from "./Contacts";

import type { Contact } from "./Contacts";

// Optional configuration
const options = {
enableVibrateFallback: true,
ignoreAndroidSystemSettings: false,
};
const haptic = () =>
trigger(Platform.OS === "ios" ? "impactLight" : "keyboardTap", options);

export default function ToolbarExample() {
const [showAutoFill, setShowAutoFill] = useState(false);
const [name, setName] = useState("");
Expand Down Expand Up @@ -145,6 +154,9 @@ export default function ToolbarExample() {
<AutoFillContacts onContactSelected={onContactSelected} />
) : null
}
onDoneCallback={haptic}
onPrevCallback={haptic}
onNextCallback={haptic}
/>
</>
);
Expand Down
5 changes: 5 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4702,6 +4702,11 @@ react-native-gesture-handler@2.15.0:
lodash "^4.17.21"
prop-types "^15.7.2"

react-native-haptic-feedback@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.2.0.tgz#bc46edd1f053265bfbe6c32487cbce074e099429"
integrity sha512-3tqJOjCguWhIrX0nkURn4yw6kXdsSDjjrvZCRjKXYGlL28hdQmoW2okAHduDTD9FWj9lA+lHgwFWgGs4aFNN7A==

"react-native-keyboard-controller@link:..":
version "0.0.0"
uid ""
Expand Down
36 changes: 32 additions & 4 deletions src/components/KeyboardToolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { StyleSheet, Text, View } from "react-native";

import {
Expand Down Expand Up @@ -32,6 +32,18 @@ export type KeyboardToolbarProps = {
* and want to show only `Done` button. Default to `true`.
*/
showArrows?: boolean;
/**
* A callback that is called when the user presses the next button along with the default action.
*/
onNextCallback?: () => void;
/**
* A callback that is called when the user presses the previous button along with the default action.
*/
onPrevCallback?: () => void;
/**
* A callback that is called when the user presses the done button along with the default action.
*/
onDoneCallback?: () => void;
};
const TEST_ID_KEYBOARD_TOOLBAR = "keyboard.toolbar";
const TEST_ID_KEYBOARD_TOOLBAR_PREVIOUS = `${TEST_ID_KEYBOARD_TOOLBAR}.previous`;
Expand All @@ -57,6 +69,9 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> = ({
button,
icon,
showArrows = true,
onNextCallback,
onPrevCallback,
onDoneCallback,
}) => {
const colorScheme = useColorScheme();
const [inputs, setInputs] = useState({
Expand Down Expand Up @@ -89,6 +104,19 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> = ({
const ButtonContainer = button || Button;
const IconContainer = icon || Arrow;

const onPressNext = useCallback(() => {
goToNextField();
onNextCallback?.();
}, [onNextCallback]);
const onPressPrev = useCallback(() => {
goToPrevField();
onPrevCallback?.();
}, [onPrevCallback]);
const onPressDone = useCallback(() => {
dismissKeyboard();
onDoneCallback?.();
}, [onDoneCallback]);

return (
<KeyboardStickyView offset={offset}>
<View style={toolbarStyle} testID={TEST_ID_KEYBOARD_TOOLBAR}>
Expand All @@ -98,7 +126,7 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> = ({
accessibilityLabel="Previous"
accessibilityHint="Will move focus to previous field"
disabled={isPrevDisabled}
onPress={goToPrevField}
onPress={onPressPrev}
testID={TEST_ID_KEYBOARD_TOOLBAR_PREVIOUS}
theme={theme}
>
Expand All @@ -112,7 +140,7 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> = ({
accessibilityLabel="Next"
accessibilityHint="Will move focus to next field"
disabled={isNextDisabled}
onPress={goToNextField}
onPress={onPressNext}
testID={TEST_ID_KEYBOARD_TOOLBAR_NEXT}
theme={theme}
>
Expand All @@ -131,7 +159,7 @@ const KeyboardToolbar: React.FC<KeyboardToolbarProps> = ({
<ButtonContainer
accessibilityLabel="Done"
accessibilityHint="Will close the keyboard"
onPress={dismissKeyboard}
onPress={onPressDone}
testID={TEST_ID_KEYBOARD_TOOLBAR_DONE}
rippleRadius={28}
style={styles.doneButtonContainer}
Expand Down

0 comments on commit 7d2ccb5

Please sign in to comment.