Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: focused input #257

Merged
merged 8 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions FabricExample/__tests__/focused-input.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import '@testing-library/jest-native/extend-expect';
import React from 'react';
import Reanimated, { useAnimatedStyle } from 'react-native-reanimated';
import { render } from '@testing-library/react-native';

import { useReanimatedFocusedInput } from 'react-native-keyboard-controller';

function RectangleWithFocusedInputLayout() {
const { input } = useReanimatedFocusedInput();
const style = useAnimatedStyle(
() => {
const layout = input.value?.layout;

return {
top: layout?.y,
left: layout?.x,
height: layout?.height,
width: layout?.width,
};
},
[]
);

return <Reanimated.View testID="view" style={style} />;
}

describe('`useReanimatedFocusedInput` mocking', () => {
it('should have different styles depends on `useReanimatedFocusedInput`', () => {
const { getByTestId, update } = render(<RectangleWithFocusedInputLayout />);

expect(getByTestId('view')).toHaveStyle({
top: 0,
left: 0,
width: 200,
height: 40,
});

(useReanimatedFocusedInput as jest.Mock).mockReturnValue({
input: {
value: {
target: 2,
layout: {
x: 10,
y: 100,
width: 190,
height: 80,
absoluteX: 100,
absoluteY: 200,
},
},
}
});
update(<RectangleWithFocusedInputLayout />);

expect(getByTestId('view')).toHaveStyle({
top: 100,
left: 10,
width: 190,
height: 80,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import React, { FC } from 'react';
import { ScrollViewProps, useWindowDimensions } from 'react-native';
import { FocusedInputLayoutChangedEvent, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import Reanimated, {
MeasuredDimensions,
interpolate,
scrollTo,
useAnimatedReaction,
useAnimatedRef,
useAnimatedScrollHandler,
useAnimatedStyle,
useSharedValue,
useWorkletCallback,
} from 'react-native-reanimated';
import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler';
import { AwareScrollViewProvider, useAwareScrollView } from './context';

const BOTTOM_OFFSET = 50;

type KeyboardAwareScrollViewProps = ScrollViewProps;

/**
* Everything begins from `onStart` handler. This handler is called every time,
* when keyboard changes its size or when focused `TextInput` was changed. In
Expand Down Expand Up @@ -55,22 +53,21 @@ type KeyboardAwareScrollViewProps = ScrollViewProps;
* +============================+ +============================+ +=====================================+
*
*/
const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
const KeyboardAwareScrollView: FC<ScrollViewProps> = ({
children,
...rest
}) => {
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();
const scrollPosition = useSharedValue(0);
const position = useSharedValue(0);
const layout = useSharedValue<MeasuredDimensions | null>(null);
const fakeViewHeight = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const tag = useSharedValue(-1);
const initialKeyboardSize = useSharedValue(0);
const scrollBeforeKeyboardMovement = useSharedValue(0);
const { input } = useReanimatedFocusedInput();
const layout = useSharedValue<FocusedInputLayoutChangedEvent | null>(null);

const { height } = useWindowDimensions();
const { measure } = useAwareScrollView();

const onScroll = useAnimatedScrollHandler(
{
Expand All @@ -84,10 +81,9 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
/**
* Function that will scroll a ScrollView as keyboard gets moving
*/
const maybeScroll = useWorkletCallback((e: number, animated = false) => {
fakeViewHeight.value = e;
const maybeScroll = useWorkletCallback((e: number, animated: boolean = false) => {
const visibleRect = height - keyboardHeight.value;
const point = (layout.value?.pageY || 0) + (layout.value?.height || 0);
const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);

if (visibleRect - point <= BOTTOM_OFFSET) {
const interpolatedScrollTo = interpolate(
Expand All @@ -98,7 +94,11 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
const targetScrollY =
Math.max(interpolatedScrollTo, 0) + scrollPosition.value;
scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated);

return interpolatedScrollTo;
}

return 0;
}, []);

useSmoothKeyboardHandler(
Expand All @@ -110,6 +110,8 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
keyboardHeight.value !== e.height && e.height > 0;
const keyboardWillAppear = e.height > 0 && keyboardHeight.value === 0;
const keyboardWillHide = e.height === 0;
const focusWasChanged = (tag.value !== e.target && e.target !== -1) || keyboardWillChangeSize;

if (keyboardWillChangeSize) {
initialKeyboardSize.value = keyboardHeight.value;
}
Expand All @@ -120,24 +122,28 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
scrollPosition.value = scrollBeforeKeyboardMovement.value;
}

if (keyboardWillAppear || keyboardWillChangeSize) {
if (keyboardWillAppear || keyboardWillChangeSize || focusWasChanged) {
// persist scroll value
scrollPosition.value = position.value;
// just persist height - later will be used in interpolation
keyboardHeight.value = e.height;
}

// focus was changed
if (tag.value !== e.target || keyboardWillChangeSize) {
if (focusWasChanged) {
tag.value = e.target;

if (tag.value !== -1) {
// save position of focused text input when keyboard starts to move
layout.value = measure(e.target);
// save current scroll position - when keyboard will hide we'll reuse
// this value to achieve smooth hide effect
scrollBeforeKeyboardMovement.value = position.value;
}
// save position of focused text input when keyboard starts to move
layout.value = input.value;
// save current scroll position - when keyboard will hide we'll reuse
// this value to achieve smooth hide effect
scrollBeforeKeyboardMovement.value = position.value;
}

if (focusWasChanged && !keyboardWillAppear) {
// update position on scroll value, so `onEnd` handler
// will pick up correct values
position.value += maybeScroll(e.height, true);
}
},
onMove: (e) => {
Expand All @@ -150,24 +156,24 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({

keyboardHeight.value = e.height;
scrollPosition.value = position.value;

if (e.target !== -1 && e.height !== 0) {
const prevLayout = layout.value;
// just be sure, that view is no overlapped (i.e. focus changed)
layout.value = measure(e.target);
maybeScroll(e.height, true);
// do layout substitution back to assure there will be correct
// back transition when keyboard hides
layout.value = prevLayout;
}
},
},
[height]
);

useAnimatedReaction(() => input.value, (current, previous) => {
if (current?.target === previous?.target && current?.layout.height !== previous?.layout.height) {
const prevLayout = layout.value;

layout.value = input.value;
scrollPosition.value += maybeScroll(keyboardHeight.value, true);
layout.value = prevLayout;
}
}, []);

const view = useAnimatedStyle(
() => ({
height: fakeViewHeight.value,
paddingBottom: keyboardHeight.value,
}),
[]
);
Expand All @@ -179,16 +185,11 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({
onScroll={onScroll}
scrollEventThrottle={16}
>
{children}
<Reanimated.View style={view} />
<Reanimated.View style={view}>
{children}
</Reanimated.View>
</Reanimated.ScrollView>
);
};

export default function (props: KeyboardAwareScrollViewProps) {
return (
<AwareScrollViewProvider>
<KeyboardAwareScrollView {...props} />
</AwareScrollViewProvider>
);
}
export default KeyboardAwareScrollView;
41 changes: 22 additions & 19 deletions FabricExample/src/screens/Examples/AwareScrollView/TextInput.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import React from 'react';
import { TextInputProps, TextInput as TextInputRN } from 'react-native';
import { randomColor } from '../../../utils';
import { useAwareScrollView } from './context';

const TextInput = React.forwardRef((props: TextInputProps, forwardRef) => {
const { onRef } = useAwareScrollView();
import { StyleSheet, TextInputProps, TextInput as TextInputRN } from 'react-native';

const TextInput = (props: TextInputProps) => {
return (
<TextInputRN
ref={(ref) => {
onRef(ref);
if (typeof forwardRef === 'function') {
forwardRef(ref);
}
}}
placeholderTextColor="black"
style={{
width: '100%',
height: 50,
backgroundColor: randomColor(),
marginTop: 50,
}}
placeholderTextColor="#6c6c6c"
style={styles.container}
multiline
numberOfLines={10}
{...props}
placeholder={`${props.placeholder} (${props.keyboardType === 'default' ? 'text' : 'numeric'})`}
/>
);
};

const styles = StyleSheet.create({
container: {
width: '100%',
minHeight: 50,
maxHeight: 200,
marginBottom: 50,
borderColor: 'black',
borderWidth: 2,
marginRight: 160,
borderRadius: 10,
color: 'black',
paddingHorizontal: 12,
},
});

export default TextInput;
102 changes: 0 additions & 102 deletions FabricExample/src/screens/Examples/AwareScrollView/context.tsx

This file was deleted.

Loading