Skip to content

Commit

Permalink
feat: add enabled/disabled state for KeyboardAwareScrollView component (
Browse files Browse the repository at this point in the history
#350)

## 📜 Description

I added the enableKeyboardHandling property to be able to turn off this
component on screens that are no longer in focus and save resources.
Also, it can be useful if someone uses bottomSheet navigation or has
text input in a transparent modal or alerts.

## 💡 Motivation and Context

To give an ability for users to turn off auto scrolling (sometimes it
may be needed to cover specific requirements).

## 📢 Changelog

- added new `enabled` prop for `KeyboardAwareScrollView` component;

## 🤔 How Has This Been Tested?

Tested on:
- iPhone 15 pro
- Pixel 3 a

## 📸 Screenshots (if appropriate):


https://github.com/kirillzyusko/react-native-keyboard-controller/assets/86000012/584a63f0-cafc-4cf2-b207-6e4c33cbf86c

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed

---------

Co-authored-by: kirillzyusko <zyusko.kirik@gmail.com>
  • Loading branch information
IvanIhnatsiuk and kirillzyusko committed Feb 1, 2024
1 parent 5913699 commit 3922158
Show file tree
Hide file tree
Showing 35 changed files with 296 additions and 115 deletions.
Expand Up @@ -24,6 +24,7 @@ exports[`components rendering should render \`KeyboardAvoidingView\` 1`] = `
exports[`components rendering should render \`KeyboardAwareScrollView\` 1`] = `
<RCTScrollView
bottomOffset={20}
enabled={true}
style={
{
"marginBottom": 20,
Expand Down
2 changes: 1 addition & 1 deletion FabricExample/__tests__/components-rendering.spec.tsx
Expand Up @@ -37,7 +37,7 @@ function KeyboardAvoidingViewTest() {

function KeyboardAwareScrollViewTest() {
return (
<KeyboardAwareScrollView bottomOffset={20} style={style}>
<KeyboardAwareScrollView enabled={true} bottomOffset={20} style={style}>
<EmptyView />
</KeyboardAwareScrollView>
);
Expand Down
5 changes: 3 additions & 2 deletions FabricExample/package.json
Expand Up @@ -10,6 +10,7 @@
"postinstall": "patch-package"
},
"dependencies": {
"@gorhom/bottom-sheet": "^4.6.0",
"@react-native-masked-view/masked-view": "^0.2.9",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7",
Expand All @@ -29,13 +30,13 @@
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native/metro-config": "^0.72.11",
"@testing-library/jest-native": "^5.3.3",
"@testing-library/react-native": "^11.5.0",
"@react-native/metro-config": "^0.72.11",
"@tsconfig/react-native": "^3.0.0",
"@types/react": "^18.0.24",
"babel-jest": "^29.2.1",
"jest": "^29.2.1",
"@types/react": "^18.0.24",
"metro-react-native-babel-preset": "0.76.8",
"patch-package": "^6.4.7",
"react-test-renderer": "18.2.0"
Expand Down
9 changes: 6 additions & 3 deletions FabricExample/src/App.tsx
@@ -1,5 +1,6 @@
import "react-native-gesture-handler";

import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { NavigationContainer } from "@react-navigation/native";
import * as React from "react";
import { ActivityIndicator, StyleSheet } from "react-native";
Expand Down Expand Up @@ -41,9 +42,11 @@ export default function App() {
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
<GestureHandlerRootView style={styles.root}>
<KeyboardProvider statusBarTranslucent>
<NavigationContainer linking={linking} fallback={spinner}>
<RootStack />
</NavigationContainer>
<BottomSheetModalProvider>
<NavigationContainer linking={linking} fallback={spinner}>
<RootStack />
</NavigationContainer>
</BottomSheetModalProvider>
</KeyboardProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
Expand Down
82 changes: 62 additions & 20 deletions FabricExample/src/screens/Examples/AwareScrollView/index.tsx
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { Text } from "react-native";
import { BottomSheetModal } from "@gorhom/bottom-sheet";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Button, Switch, Text, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";

import TextInput from "../../../components/TextInput";
Expand All @@ -12,38 +13,79 @@ import type { StackScreenProps } from "@react-navigation/stack";
type Props = StackScreenProps<ExamplesStackParamList>;

export default function AwareScrollView({ navigation }: Props) {
const bottomSheetModalRef = useRef<BottomSheetModal>(null);

const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.present();
}, []);

const [disableScrollOnKeyboardHide, setDisableScrollOnKeyboardHide] =
useState(false);
const [enabled, setEnabled] = useState(true);

useEffect(() => {
navigation.setOptions({
headerRight: () => (
<Text
style={styles.header}
onPress={() => setDisableScrollOnKeyboardHide((value) => !value)}
testID="disable_scroll_on_keyboard_hide"
onPress={handlePresentModalPress}
testID="open_bottom_sheet_modal"
>
{`Back scroll: ${!disableScrollOnKeyboardHide ? "true" : "false"}`}
Open config
</Text>
),
});
}, [disableScrollOnKeyboardHide]);
}, []);

return (
<KeyboardAwareScrollView
testID="aware_scroll_view_container"
bottomOffset={50}
disableScrollOnKeyboardHide={disableScrollOnKeyboardHide}
style={styles.container}
contentContainerStyle={styles.content}
>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
placeholder={`TextInput#${i}`}
keyboardType={i % 2 === 0 ? "numeric" : "default"}
<>
<KeyboardAwareScrollView
testID="aware_scroll_view_container"
bottomOffset={50}
enabled={enabled}
disableScrollOnKeyboardHide={disableScrollOnKeyboardHide}
style={styles.container}
contentContainerStyle={styles.content}
>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
placeholder={`TextInput#${i}`}
keyboardType={i % 2 === 0 ? "numeric" : "default"}
/>
))}
</KeyboardAwareScrollView>
<BottomSheetModal
snapPoints={["40%"]}
ref={bottomSheetModalRef}
index={0}
>
<Button
testID="bottom_sheet_close_modal"
title="Close modal"
onPress={() => bottomSheetModalRef.current?.close()}
/>
))}
</KeyboardAwareScrollView>
<View style={styles.switchContainer}>
<Text>Toggle back scroll</Text>
<Switch
testID="bottom_sheet_toggle_back_scroll"
value={disableScrollOnKeyboardHide}
onChange={() => {
setDisableScrollOnKeyboardHide(!disableScrollOnKeyboardHide);
}}
/>
</View>
<View style={styles.switchContainer}>
<Text>Toggle enabled</Text>
<Switch
value={enabled}
testID="bottom_sheet_toggle_enabled_state"
onChange={() => {
setEnabled(!enabled);
}}
/>
</View>
</BottomSheetModal>
</>
);
}
6 changes: 6 additions & 0 deletions FabricExample/src/screens/Examples/AwareScrollView/styles.ts
Expand Up @@ -11,4 +11,10 @@ export const styles = StyleSheet.create({
color: "black",
paddingRight: 12,
},
switchContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
margin: 16,
},
});
20 changes: 20 additions & 0 deletions FabricExample/yarn.lock
Expand Up @@ -1345,6 +1345,21 @@
dependencies:
"@types/hammerjs" "^2.0.36"

"@gorhom/bottom-sheet@^4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-4.6.0.tgz#16bebbe4f5925447ece0d727830cf1776d5620f8"
integrity sha512-XgNflkhATUqTIiMDGuLaQZAtjUzcrhGOEJGHT+7Tou1ctTMb958YRGGnU9KFo5TkD6YCZcfWfxHPi9F0FF+DjA==
dependencies:
"@gorhom/portal" "1.0.14"
invariant "^2.2.4"

"@gorhom/portal@1.0.14":
version "1.0.14"
resolved "https://registry.yarnpkg.com/@gorhom/portal/-/portal-1.0.14.tgz#1953edb76aaba80fb24021dc774550194a18e111"
integrity sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==
dependencies:
nanoid "^3.3.1"

"@hapi/hoek@^9.0.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
Expand Down Expand Up @@ -4753,6 +4768,11 @@ nanoid@^3.1.23:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==

nanoid@^3.3.1:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==

natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/api/components/keyboard-aware-scroll-view.mdx
Expand Up @@ -71,6 +71,10 @@ The distance between keyboard and focused `TextInput` when keyboard is shown. De

Prevents automatic scrolling of the `ScrollView` when the keyboard gets hidden, maintaining the current screen position. Default is `false`.

### `enabled`

A boolean prop indicating whether `KeyboardAwareScrollView` is enabled or disabled. Default is `true`.

## Example

```tsx
Expand Down
Expand Up @@ -71,6 +71,10 @@ The distance between keyboard and focused `TextInput` when keyboard is shown. De

Prevents automatic scrolling of the `ScrollView` when the keyboard gets hidden, maintaining the current screen position. Default is `false`.

### `enabled`

A boolean prop indicating whether `KeyboardAwareScrollView` is enabled or disabled. Default is `true`.

## Example

```tsx
Expand Down
24 changes: 23 additions & 1 deletion e2e/kit/002-aware-scroll-view.e2e.ts
Expand Up @@ -77,7 +77,9 @@ describe("AwareScrollView test cases", () => {
});

it("shouldn't scroll back when keyboard dismissed if such behavior intentionally disabled", async () => {
await waitAndTap("disable_scroll_on_keyboard_hide");
await waitAndTap("open_bottom_sheet_modal");
await waitAndTap("bottom_sheet_toggle_back_scroll");
await waitAndTap("bottom_sheet_close_modal");
await waitAndTap("TextInput#5");
await waitForExpect(async () => {
await expectBitmapsToBeEqual(
Expand All @@ -93,4 +95,24 @@ describe("AwareScrollView test cases", () => {
);
});
});

it("shouldn't auto scroll if `KeyboardAwareScrollView` is disabled", async () => {
await waitAndTap("open_bottom_sheet_modal");
await waitAndTap("bottom_sheet_toggle_enabled_state");
await waitAndTap("bottom_sheet_close_modal");
await waitAndTap("TextInput#7");
await waitForExpect(async () => {
await expectBitmapsToBeEqual(
"AwareScrollViewDisabledStateKeyboardOpened",
BLINKING_CURSOR,
);
});
await closeKeyboard();
await waitForExpect(async () => {
await expectBitmapsToBeEqual(
"AwareScrollViewDisabledStateKeyboardClosed",
BLINKING_CURSOR,
);
});
});
});
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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/AwareScrollViewTextChanged.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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/AwareScrollViewInputChanged.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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/AwareScrollViewTextChanged.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Expand Up @@ -25,6 +25,7 @@ exports[`components rendering should render \`KeyboardAwareScrollView\` 1`] = `
<RCTScrollView
bottomOffset={20}
disableScrollOnKeyboardHide={false}
enabled={true}
style={
{
"marginBottom": 20,
Expand Down
1 change: 1 addition & 0 deletions example/__tests__/components-rendering.spec.tsx
Expand Up @@ -39,6 +39,7 @@ function KeyboardAwareScrollViewTest() {
return (
<KeyboardAwareScrollView
bottomOffset={20}
enabled={true}
disableScrollOnKeyboardHide={false}
style={style}
>
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Expand Up @@ -11,6 +11,7 @@
"postinstall": "patch-package"
},
"dependencies": {
"@gorhom/bottom-sheet": "^4.6.0",
"@react-native-masked-view/masked-view": "^0.2.9",
"@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7",
Expand Down
78 changes: 58 additions & 20 deletions example/src/screens/Examples/AwareScrollView/index.tsx
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { Text } from "react-native";
import BottomSheet from "@gorhom/bottom-sheet";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Button, Switch, Text, View } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";

import TextInput from "../../../components/TextInput";
Expand All @@ -12,38 +13,75 @@ import type { StackScreenProps } from "@react-navigation/stack";
type Props = StackScreenProps<ExamplesStackParamList>;

export default function AwareScrollView({ navigation }: Props) {
const bottomSheetModalRef = useRef<BottomSheet>(null);

const handlePresentModalPress = useCallback(() => {
bottomSheetModalRef.current?.expand();
}, []);

const [disableScrollOnKeyboardHide, setDisableScrollOnKeyboardHide] =
useState(false);
const [enabled, setEnabled] = useState(true);

useEffect(() => {
navigation.setOptions({
headerRight: () => (
<Text
style={styles.header}
onPress={() => setDisableScrollOnKeyboardHide((value) => !value)}
testID="disable_scroll_on_keyboard_hide"
onPress={handlePresentModalPress}
testID="open_bottom_sheet_modal"
>
{`Back scroll: ${!disableScrollOnKeyboardHide ? "true" : "false"}`}
Open config
</Text>
),
});
}, [disableScrollOnKeyboardHide]);
}, []);

return (
<KeyboardAwareScrollView
testID="aware_scroll_view_container"
bottomOffset={50}
disableScrollOnKeyboardHide={disableScrollOnKeyboardHide}
style={styles.container}
contentContainerStyle={styles.content}
>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
placeholder={`TextInput#${i}`}
keyboardType={i % 2 === 0 ? "numeric" : "default"}
<>
<KeyboardAwareScrollView
testID="aware_scroll_view_container"
bottomOffset={50}
enabled={enabled}
disableScrollOnKeyboardHide={disableScrollOnKeyboardHide}
style={styles.container}
contentContainerStyle={styles.content}
>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
placeholder={`TextInput#${i}`}
keyboardType={i % 2 === 0 ? "numeric" : "default"}
/>
))}
</KeyboardAwareScrollView>
<BottomSheet snapPoints={["40%"]} ref={bottomSheetModalRef} index={-1}>
<Button
testID="bottom_sheet_close_modal"
title="Close modal"
onPress={() => bottomSheetModalRef.current?.close()}
/>
))}
</KeyboardAwareScrollView>
<View style={styles.switchContainer}>
<Text>Toggle back scroll</Text>
<Switch
testID="bottom_sheet_toggle_back_scroll"
value={disableScrollOnKeyboardHide}
onChange={() => {
setDisableScrollOnKeyboardHide(!disableScrollOnKeyboardHide);
}}
/>
</View>
<View style={styles.switchContainer}>
<Text>Toggle enabled</Text>
<Switch
value={enabled}
testID="bottom_sheet_toggle_enabled_state"
onChange={() => {
setEnabled(!enabled);
}}
/>
</View>
</BottomSheet>
</>
);
}

0 comments on commit 3922158

Please sign in to comment.