Skip to content

Commit

Permalink
feat: add KeyboardScrollView
Browse files Browse the repository at this point in the history
  • Loading branch information
rlemasquerier committed Sep 26, 2023
1 parent 5eed33b commit 5421025
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 7 deletions.
10 changes: 7 additions & 3 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import * as React from 'react';

import { StyleSheet, View, Text, ScrollView } from 'react-native';
import { StyleSheet, View, Text } from 'react-native';
import { TextInput } from './components/TextInput';
import { ContentCard } from './components/ContentCard';
import { KeyboardScrollView } from 'react-native-keyboard-scrollview';

export default function App() {
return (
<View style={styles.appContainer}>
<View style={styles.header}>
<Text style={styles.headerText}>Example app</Text>
</View>
<ScrollView contentContainerStyle={styles.container}>
<KeyboardScrollView
contentContainerStyle={styles.container}
additionalScrollHeight={20}
>
<ContentCard />
<ContentCard />
<TextInput />
</ScrollView>
</KeyboardScrollView>
</View>
);
}
Expand Down
134 changes: 134 additions & 0 deletions src/KeyboardScrollView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Keyboard,
ScrollView,
TextInput,
StatusBar,
StyleSheet,
} from 'react-native';

interface Props extends React.ComponentProps<typeof ScrollView> {
additionalScrollHeight?: number;
}

export const KeyboardScrollView = ({
children,
additionalScrollHeight,
...props
}: Props) => {
const scrollViewRef = useRef<ScrollView>(null);
const scrollPositionRef = useRef<number>(0);
const scrollContentSizeRef = useRef<number>(0);
const scrollViewSizeRef = useRef<number>(0);

const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
const [additionalPadding, setAdditionalPadding] = useState(0);

const scrollToPosition = useCallback(
(toPosition: number, animated?: boolean) => {
scrollViewRef.current?.scrollTo({ y: toPosition, animated: !!animated });
scrollPositionRef.current = toPosition;
},
[]
);

const additionalScroll = useMemo(
() => additionalScrollHeight ?? 0,
[additionalScrollHeight]
);
const androidStatusBarOffset = useMemo(
() => StatusBar.currentHeight ?? 0,
[]
);

useEffect(() => {
Keyboard.addListener('keyboardDidShow', (frames) => {
const keyboardY = frames.endCoordinates.screenY;
const keyboardHeight = frames.endCoordinates.height;
setAdditionalPadding(keyboardHeight);
setTimeout(() => {
setIsKeyboardVisible(true);
}, 100);

const currentlyFocusedInput = TextInput.State.currentlyFocusedInput();
const currentScrollY = scrollPositionRef.current;

currentlyFocusedInput.measureInWindow((_x, y, _width, height) => {
const endOfInputY = y + height + androidStatusBarOffset;
const deltaToScroll = endOfInputY - keyboardY;

if (deltaToScroll < 0) return;

const scrollPositionTarget =
currentScrollY + deltaToScroll + additionalScroll;
scrollToPosition(scrollPositionTarget, true);
});
});

Keyboard.addListener('keyboardDidHide', () => {
setAdditionalPadding(0);
setIsKeyboardVisible(false);
});

Keyboard.addListener('keyboardWillHide', (frames) => {
// iOS only, scroll back to initial position to avoid flickering
const keyboardHeight = frames.endCoordinates.height;
const currentScrollY = scrollPositionRef.current;
const scrollPositionTarget = currentScrollY - keyboardHeight;
scrollToPosition(scrollPositionTarget, true);
});

return () => {
Keyboard.removeAllListeners('keyboardDidShow');
Keyboard.removeAllListeners('keyboardDidHide');
Keyboard.removeAllListeners('keyboardWillHide');
};
}, [additionalScroll, androidStatusBarOffset, scrollToPosition]);

return (
<ScrollView
ref={scrollViewRef}
contentContainerStyle={[
styles.contentContainer,
{ paddingBottom: additionalPadding },
]}
keyboardShouldPersistTaps="handled"
onMomentumScrollEnd={(event) => {
scrollPositionRef.current = event.nativeEvent.contentOffset.y;
}}
onScrollEndDrag={(event) => {
scrollPositionRef.current = event.nativeEvent.contentOffset.y;
}}
onLayout={(event) => {
scrollViewSizeRef.current = event.nativeEvent.layout.height;
}}
onContentSizeChange={(_width, height) => {
const currentContentHeight = scrollContentSizeRef.current;
const contentSizeDelta = height - currentContentHeight;
scrollContentSizeRef.current = height;

if (!isKeyboardVisible) return;

const currentScrollY = scrollPositionRef.current;
const scrollPositionTarget = currentScrollY + contentSizeDelta;
scrollToPosition(scrollPositionTarget, true);
}}
{...props}
>
{children}
</ScrollView>
);
};

const styles = StyleSheet.create({
contentContainer: {
flexGrow: 1,
backgroundColor: 'pink',
},
});
1 change: 0 additions & 1 deletion src/__tests__/index.test.tsx

This file was deleted.

4 changes: 1 addition & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
export function multiply(a: number, b: number): Promise<number> {
return Promise.resolve(a * b);
}
export { KeyboardScrollView } from './KeyboardScrollView';

0 comments on commit 5421025

Please sign in to comment.