diff --git a/example/src/App.tsx b/example/src/App.tsx index a527dbd..c0fec14 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,8 +1,9 @@ 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 ( @@ -10,11 +11,14 @@ export default function App() { Example app - + - + ); } diff --git a/src/KeyboardScrollView.tsx b/src/KeyboardScrollView.tsx new file mode 100644 index 0000000..236b676 --- /dev/null +++ b/src/KeyboardScrollView.tsx @@ -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 { + additionalScrollHeight?: number; +} + +export const KeyboardScrollView = ({ + children, + additionalScrollHeight, + ...props +}: Props) => { + const scrollViewRef = useRef(null); + const scrollPositionRef = useRef(0); + const scrollContentSizeRef = useRef(0); + const scrollViewSizeRef = useRef(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 ( + { + 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} + + ); +}; + +const styles = StyleSheet.create({ + contentContainer: { + flexGrow: 1, + backgroundColor: 'pink', + }, +}); diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx deleted file mode 100644 index bf84291..0000000 --- a/src/__tests__/index.test.tsx +++ /dev/null @@ -1 +0,0 @@ -it.todo('write a test'); diff --git a/src/index.tsx b/src/index.tsx index 9e42cf3..3c0ca20 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,3 +1 @@ -export function multiply(a: number, b: number): Promise { - return Promise.resolve(a * b); -} +export { KeyboardScrollView } from './KeyboardScrollView';