Skip to content
This repository was archived by the owner on Mar 24, 2026. It is now read-only.

Commit 78121ce

Browse files
committed
fix: fix somme issues when scroll view not fitting all screen
1 parent 17c971b commit 78121ce

6 files changed

Lines changed: 147 additions & 126 deletions

File tree

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,31 @@ A react-native hook that handle auto-scroll when input is focused and behind key
55
## Installation
66

77
```sh
8-
npm install react-native-use-input-scroll-handler
8+
yarn add react-native-use-input-scroll-handler
99
```
1010

1111
## Usage
1212

1313
```js
14-
import UseInputScrollHandler from "react-native-use-input-scroll-handler";
14+
import useInputScrollHandler from "react-native-use-input-scroll-handler";
1515

1616
// ...
1717

18-
const result = await UseInputScrollHandler.multiply(3, 7);
18+
const { scrollHandler } = useInputScrollHandler();
19+
20+
<ScrollView {...scrollHandler}>
1921
```
2022

23+
## API
24+
25+
Custom options can be passed to this hook.
26+
27+
| **Option** | **Type** | **Description** |
28+
| --------------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------- |
29+
| `ref` | `MutableRefObject` | Use this if you need to use your own scroll view ref. |
30+
| `extraScrollHeight` | `number` | Adds an extra offset to the keyboard. Useful if you want to stick elements above the keyboard. |
31+
| `keyboardOpeningTime` | `number` | Sets the delay time before scrolling to new position, default is 250 |
32+
2133
## Contributing
2234

2335
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.

example/src/App.tsx

Lines changed: 41 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,59 @@
11
import * as React from 'react';
2-
3-
import { StyleSheet, ScrollView, View, TextInput } from 'react-native';
2+
import { StyleSheet, ScrollView, SafeAreaView } from 'react-native';
43
import useInputScrollHandler from 'react-native-use-input-scroll-handler';
54

5+
import Input from './atoms/Input';
6+
67
export default function App() {
78
const { scrollHandler } = useInputScrollHandler();
89

910
return (
10-
<View style={styles.container}>
11-
<ScrollView style={styles.container} {...scrollHandler}>
12-
<TextInput placeholder="Test" />
13-
<TextInput placeholder="Test" />
14-
<TextInput placeholder="Test" />
15-
<TextInput placeholder="Test" />
16-
<TextInput placeholder="Test" />
17-
<TextInput placeholder="Test" />
18-
<TextInput placeholder="Test" />
19-
<TextInput placeholder="Test" />
20-
<TextInput placeholder="Test" />
21-
<TextInput placeholder="Test" />
22-
<TextInput placeholder="Test" />
23-
<TextInput placeholder="Test" />
24-
<TextInput placeholder="Test" />
25-
<TextInput placeholder="Test" />
26-
<TextInput placeholder="Test" />
27-
<TextInput placeholder="Test" />
28-
<TextInput placeholder="Test" />
29-
<TextInput placeholder="Test" />
30-
<TextInput placeholder="Test" />
31-
<TextInput placeholder="Test" />
32-
<TextInput placeholder="Test" />
33-
<TextInput placeholder="Test" />
34-
<TextInput placeholder="Test" />
35-
<TextInput placeholder="Test" />
36-
<TextInput placeholder="Test" />
37-
<TextInput placeholder="Test" />
38-
<TextInput placeholder="Test" />
39-
<TextInput placeholder="Test" />
40-
<TextInput placeholder="Test" />
41-
<TextInput placeholder="Test" />
42-
<TextInput placeholder="Test" />
43-
<TextInput placeholder="Test" />
44-
<TextInput placeholder="Test" />
45-
<TextInput placeholder="Test" />
46-
<TextInput placeholder="Test" />
47-
<TextInput placeholder="Test" />
48-
<TextInput placeholder="Test" />
49-
<TextInput placeholder="Test" />
50-
<TextInput placeholder="Test" />
51-
<TextInput placeholder="Test" />
11+
<SafeAreaView style={styles.container}>
12+
<ScrollView style={styles.scrollView} {...scrollHandler}>
13+
<Input placeholder="Test" />
14+
<Input placeholder="Test" />
15+
<Input placeholder="Test" />
16+
<Input placeholder="Test" />
17+
<Input placeholder="Test" />
18+
<Input placeholder="Test" />
19+
<Input placeholder="Test" />
20+
<Input placeholder="Test" />
21+
<Input placeholder="Test" />
22+
<Input placeholder="Test" />
23+
<Input placeholder="Test" />
24+
<Input placeholder="Test" />
25+
<Input placeholder="Test" />
26+
<Input placeholder="Test" />
27+
<Input placeholder="Test" />
28+
<Input placeholder="Test" />
29+
<Input placeholder="Test" />
30+
<Input placeholder="Test" />
31+
<Input placeholder="Test" />
32+
<Input placeholder="Test" />
33+
<Input placeholder="Test" />
34+
<Input placeholder="Test" />
35+
<Input placeholder="Test" />
36+
<Input placeholder="Test" />
37+
<Input placeholder="Test" />
38+
<Input placeholder="Test" />
39+
<Input placeholder="Test" />
40+
<Input placeholder="Test" />
41+
<Input placeholder="Test" />
42+
<Input placeholder="Test" />
43+
<Input placeholder="Test" />
44+
<Input placeholder="Test" />
5245
</ScrollView>
53-
<View style={styles.footer} />
54-
</View>
46+
</SafeAreaView>
5547
);
5648
}
5749

5850
const styles = StyleSheet.create({
5951
container: {
6052
flex: 1,
6153
},
54+
scrollView: {
55+
flex: 1,
56+
},
6257
box: {
6358
width: 60,
6459
height: 60,

example/src/atoms/Input.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React, { ComponentProps } from 'react';
2+
import { TextInput, StyleSheet } from 'react-native';
3+
4+
const styles = StyleSheet.create({
5+
input: {
6+
height: 40,
7+
borderWidth: 1,
8+
borderColor: 'grey',
9+
borderRadius: 5,
10+
padding: 10,
11+
marginHorizontal: 16,
12+
marginBottom: 8,
13+
},
14+
});
15+
16+
interface Props extends ComponentProps<typeof TextInput> {}
17+
18+
const Input = ({ style, ...rest }: Props) => (
19+
<TextInput {...rest} style={[styles.input, style]} />
20+
);
21+
22+
export default Input;

src/__tests__/useLayout.test.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/index.ts

Lines changed: 69 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useRef, useEffect, useState } from 'react';
1+
import { useRef, useEffect, useState, MutableRefObject } from 'react';
22
import {
33
ScrollView,
44
UIManager,
@@ -14,8 +14,6 @@ import {
1414
useWindowDimensions,
1515
} from 'react-native';
1616

17-
import { useLayout } from './utils/useLayout';
18-
1917
const _keyboardWillShow: KeyboardEventName = Platform.select({
2018
ios: 'keyboardWillShow',
2119
android: 'keyboardDidShow',
@@ -28,26 +26,23 @@ const _keyboardWillHide: KeyboardEventName = Platform.select({
2826
});
2927

3028
export interface Options {
29+
ref?: MutableRefObject<ScrollView>;
3130
extraScrollHeight?: number;
3231
keyboardOpeningTime?: number;
3332
}
3433

3534
export default function useInputScrollHandler(options: Options = {}) {
3635
// Refs
37-
const ref = useRef<ScrollView>(null);
36+
const ref = useRef<ScrollView>(options.ref?.current ?? null);
3837
const offset = useRef<NativeScrollPoint>();
3938
const handledOffset = useRef<NativeScrollPoint>();
4039

4140
// Window
4241
const screen = useWindowDimensions();
4342

44-
// Layout
45-
const { onLayout, ...layout } = useLayout();
46-
4743
// Variables
4844
const extraScrollHeight = options?.extraScrollHeight ?? 0;
4945
const keyboardOpeningTime = options?.keyboardOpeningTime ?? 250;
50-
const bottomSpace = screen.height - (layout.y + layout.height);
5146

5247
// States
5348
const [keyboardSpace, setKeyboardSpace] = useState(0);
@@ -59,23 +54,26 @@ export default function useInputScrollHandler(options: Options = {}) {
5954
return responder;
6055
};
6156

62-
const scrollToFocusedInput = (reactNode: any) => {
57+
const scrollToFocusedInput = (reactNode: any, additionalOffset: number) => {
6358
setTimeout(() => {
6459
const responder = getScrollResponder();
6560

6661
responder &&
6762
responder.scrollResponderScrollNativeHandleToKeyboard(
6863
reactNode,
69-
extraScrollHeight,
70-
true
64+
additionalOffset + extraScrollHeight,
65+
false
7166
);
7267
}, keyboardOpeningTime);
7368
};
7469

75-
const scrollToFocusedInputWithNodeHandle = (nodeID: number) => {
70+
const scrollToFocusedInputWithNodeHandle = (
71+
nodeID: number,
72+
additionalOffset: number
73+
) => {
7674
const reactNode = findNodeHandle(nodeID);
7775

78-
scrollToFocusedInput(reactNode);
76+
scrollToFocusedInput(reactNode, additionalOffset);
7977
};
8078

8179
const handleOnScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
@@ -90,47 +88,72 @@ export default function useInputScrollHandler(options: Options = {}) {
9088
: TextInput.State.currentlyFocusedField();
9189
const responder = getScrollResponder();
9290

93-
setKeyboardSpace(
94-
e.endCoordinates.height + (options.extraScrollHeight ?? 0) - bottomSpace
95-
);
96-
9791
if (!currentlyFocusedField || !responder) {
9892
return;
9993
}
10094

101-
// @ts-ignore
102-
UIManager.viewIsDescendantOf(
103-
currentlyFocusedField,
104-
responder.getInnerViewNode(),
105-
(isAncestor: boolean) => {
106-
if (isAncestor) {
107-
UIManager.measureInWindow(
108-
currentlyFocusedField,
109-
(_x: number, y: number, _width: number, height: number) => {
110-
const textInputBottomPosition = y + height;
111-
const keyboardPosition = e.endCoordinates.screenY;
112-
const totalExtraHeight = options.extraScrollHeight ?? 0;
113-
114-
if (Platform.OS === 'ios') {
115-
if (
116-
textInputBottomPosition >
117-
keyboardPosition - totalExtraHeight
118-
) {
119-
scrollToFocusedInputWithNodeHandle(currentlyFocusedField);
95+
handledOffset.current = offset.current;
96+
97+
// Calculate scroll view position
98+
UIManager.measure(
99+
responder.getScrollableNode(),
100+
(
101+
_scrollX: number,
102+
_scrollY: number,
103+
_scrollWidth: number,
104+
scrollHeight: number,
105+
_scrollPageX: number,
106+
scrollPageY: number
107+
) => {
108+
const bottomSpace = screen.height - (scrollPageY + scrollHeight);
109+
110+
setKeyboardSpace(
111+
e.endCoordinates.height + extraScrollHeight - bottomSpace
112+
);
113+
114+
// @ts-ignore
115+
UIManager.viewIsDescendantOf(
116+
currentlyFocusedField,
117+
responder.getInnerViewNode(),
118+
(isAncestor: boolean) => {
119+
if (!isAncestor) {
120+
return;
121+
}
122+
123+
UIManager.measureInWindow(
124+
currentlyFocusedField,
125+
(
126+
_inputX: number,
127+
inputY: number,
128+
_inputWidth: number,
129+
inputHeight: number
130+
) => {
131+
const textInputBottomPosition = inputY + inputHeight;
132+
const keyboardPosition = e.endCoordinates.screenY;
133+
const totalExtraHeight = extraScrollHeight;
134+
135+
if (Platform.OS === 'ios') {
136+
if (
137+
textInputBottomPosition >
138+
keyboardPosition - totalExtraHeight
139+
) {
140+
scrollToFocusedInputWithNodeHandle(
141+
currentlyFocusedField,
142+
scrollPageY
143+
);
144+
}
145+
} else {
146+
responder.scrollTo({
147+
y: (offset.current?.y ?? 0) + extraScrollHeight,
148+
animated: true,
149+
});
120150
}
121-
} else {
122-
responder.scrollTo({
123-
y: (offset.current?.y ?? 0) + extraScrollHeight,
124-
animated: true,
125-
});
126151
}
127-
}
128-
);
129-
}
152+
);
153+
}
154+
);
130155
}
131156
);
132-
133-
handledOffset.current = offset.current;
134157
};
135158

136159
const handleResetKeyboardSpace: KeyboardEventListener = () => {
@@ -165,7 +188,6 @@ export default function useInputScrollHandler(options: Options = {}) {
165188
contentContainerStyle: { paddingBottom: keyboardSpace },
166189
onScroll: handleOnScroll,
167190
scrollEventThrottle: 16,
168-
onLayout,
169191
},
170192
keyboardSpace,
171193
};

src/utils/useLayout.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)