diff --git a/e2e/kit/004-aware-scroll-view.e2e.ts b/e2e/kit/004-aware-scroll-view.e2e.ts index 2d816ff8c6..c17fc26e61 100644 --- a/e2e/kit/004-aware-scroll-view.e2e.ts +++ b/e2e/kit/004-aware-scroll-view.e2e.ts @@ -2,6 +2,7 @@ import { expectBitmapsToBeEqual } from "./asserts"; import { Env, scrollUpUntilElementIsBarelyVisible, + selectText, tap, typeText, waitAndReplace, @@ -78,7 +79,7 @@ describe("AwareScrollView test cases", () => { "aware_scroll_view_container", "TextInput#4", ); - await element(by.id("TextInput#4")).multiTap(2); + await selectText("TextInput#4"); await waitForExpect(async () => { await expectBitmapsToBeEqual( "AwareScrollViewTextSelectionChanged", @@ -89,7 +90,7 @@ describe("AwareScrollView test cases", () => { }); it("should auto-scroll when user types a text", async () => { - await element(by.id("aware_scroll_view_container")).scroll(80, "up"); + await element(by.id("aware_scroll_view_container")).scroll(40, "up"); await typeText("TextInput#4", "1"); await waitForExpect(async () => { await expectBitmapsToBeEqual( diff --git a/e2e/kit/assets/android/e2e_emulator_28/AwareScrollViewTextChanged.png b/e2e/kit/assets/android/e2e_emulator_28/AwareScrollViewTextChanged.png index 4b4e0518e5..92cdf562a9 100644 Binary files a/e2e/kit/assets/android/e2e_emulator_28/AwareScrollViewTextChanged.png and b/e2e/kit/assets/android/e2e_emulator_28/AwareScrollViewTextChanged.png differ diff --git a/e2e/kit/assets/android/e2e_emulator_31/AwareScrollViewTextChanged.png b/e2e/kit/assets/android/e2e_emulator_31/AwareScrollViewTextChanged.png index 87eb6ddd4d..36529291cf 100644 Binary files a/e2e/kit/assets/android/e2e_emulator_31/AwareScrollViewTextChanged.png and b/e2e/kit/assets/android/e2e_emulator_31/AwareScrollViewTextChanged.png differ diff --git a/e2e/kit/assets/ios/iPhone 13 Pro/AwareScrollViewTextChanged.png b/e2e/kit/assets/ios/iPhone 13 Pro/AwareScrollViewTextChanged.png index 4eb93b6b04..7aed36d845 100644 Binary files a/e2e/kit/assets/ios/iPhone 13 Pro/AwareScrollViewTextChanged.png and b/e2e/kit/assets/ios/iPhone 13 Pro/AwareScrollViewTextChanged.png differ diff --git a/e2e/kit/assets/ios/iPhone 14 Pro/AwareScrollViewTextChanged.png b/e2e/kit/assets/ios/iPhone 14 Pro/AwareScrollViewTextChanged.png index ba3eba9be5..2ef3c03ecb 100644 Binary files a/e2e/kit/assets/ios/iPhone 14 Pro/AwareScrollViewTextChanged.png and b/e2e/kit/assets/ios/iPhone 14 Pro/AwareScrollViewTextChanged.png differ diff --git a/e2e/kit/assets/ios/iPhone 15 Pro/AwareScrollViewTextChanged.png b/e2e/kit/assets/ios/iPhone 15 Pro/AwareScrollViewTextChanged.png index 1408d9dbdd..2235294ad0 100644 Binary files a/e2e/kit/assets/ios/iPhone 15 Pro/AwareScrollViewTextChanged.png and b/e2e/kit/assets/ios/iPhone 15 Pro/AwareScrollViewTextChanged.png differ diff --git a/e2e/kit/assets/ios/iPhone 16 Pro/AwareScrollViewTextChanged.png b/e2e/kit/assets/ios/iPhone 16 Pro/AwareScrollViewTextChanged.png index daadb73fea..96638c6265 100644 Binary files a/e2e/kit/assets/ios/iPhone 16 Pro/AwareScrollViewTextChanged.png and b/e2e/kit/assets/ios/iPhone 16 Pro/AwareScrollViewTextChanged.png differ diff --git a/e2e/kit/helpers/actions/index.ts b/e2e/kit/helpers/actions/index.ts index ac7cf2ea75..01fe44433e 100644 --- a/e2e/kit/helpers/actions/index.ts +++ b/e2e/kit/helpers/actions/index.ts @@ -145,12 +145,27 @@ export const scrollUpUntilElementIsBarelyVisible = async ( await element(by.id(elementId)).tap({ x: 0, y: 25 }); } } catch (e) { - await element(by.id(scrollViewId)).scroll(35, "down", 0.01, 0.5); + await element(by.id(scrollViewId)).scroll(50, "down", 0.01, 0.5); break; } } }; +export const selectText = async (id: string) => { + console.debug( + "---------------------------------\n", + "Select text with id:", + colors.magenta(id), + ); + + if (device.getPlatform() === "ios") { + // on Android multiTap sometimes may not work properly + await element(by.id(id)).multiTap(2); + } else { + await element(by.id(id)).longPress(); + } +}; + export const closeKeyboard = async (textInputId: string) => { if (device.getPlatform() === "android") { await device.pressBack(); diff --git a/src/components/KeyboardAwareScrollView/index.tsx b/src/components/KeyboardAwareScrollView/index.tsx index 10e316b88c..9b950509dd 100644 --- a/src/components/KeyboardAwareScrollView/index.tsx +++ b/src/components/KeyboardAwareScrollView/index.tsx @@ -1,6 +1,7 @@ import React, { forwardRef, useCallback, useMemo } from "react"; import { findNodeHandle } from "react-native"; import Reanimated, { + clamp, interpolate, scrollTo, useAnimatedReaction, @@ -111,6 +112,8 @@ const KeyboardAwareScrollView = forwardRef< const scrollBeforeKeyboardMovement = useSharedValue(0); const { input } = useReanimatedFocusedInput(); const layout = useSharedValue(null); + const lastSelection = + useSharedValue(null); const { height } = useWindowDimensions(); @@ -153,9 +156,13 @@ const KeyboardAwareScrollView = forwardRef< const inputHeight = layout.value?.layout.height || 0; const point = absoluteY + inputHeight; + console.log({ absoluteY, inputHeight, point, visibleRect }); + if (visibleRect - point <= bottomOffset) { const relativeScrollTo = keyboardHeight.value - (height - point) + bottomOffset; + + console.log({ relativeScrollTo }); const interpolatedScrollTo = interpolate( e, [initialKeyboardSize.value, keyboardHeight.value], @@ -170,6 +177,11 @@ const KeyboardAwareScrollView = forwardRef< const targetScrollY = Math.max(interpolatedScrollTo, 0) + scrollPosition.value; + console.log({ + targetScrollY, + scrollPosition: scrollPosition.value, + interpolatedScrollTo, + }); scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated); return interpolatedScrollTo; @@ -207,7 +219,7 @@ const KeyboardAwareScrollView = forwardRef< ); const scrollFromCurrentPosition = useCallback( - (customHeight?: number) => { + (customHeight: number) => { "worklet"; const prevScrollPosition = scrollPosition.value; @@ -222,49 +234,70 @@ const KeyboardAwareScrollView = forwardRef< ...input.value, layout: { ...input.value.layout, - height: customHeight ?? input.value.layout.height, + // when we have multiline input with limited amount of lines, then custom height can be very big + // so we clamp it to max input height + height: clamp(customHeight, 0, input.value.layout.height), }, }; scrollPosition.value = position.value; maybeScroll(keyboardHeight.value, true); scrollPosition.value = prevScrollPosition; layout.value = prevLayout; + + console.log({ customHeight }); }, [maybeScroll], ); - const onChangeText = useCallback(() => { - "worklet"; - - // if typing a text caused layout shift, then we need to ignore this handler - // because this event will be handled in `useAnimatedReaction` below - if (layout.value?.layout.height !== input.value?.layout.height) { - return; - } - - scrollFromCurrentPosition(); - }, [scrollFromCurrentPosition]); - const onSelectionChange = useCallback( - (e: FocusedInputSelectionChangedEvent) => { + const onChangeText = useCallback( + (customHeight: number) => { "worklet"; - if (e.selection.start.position !== e.selection.end.position) { - scrollFromCurrentPosition(e.selection.end.y); + // if typing a text caused layout shift, then we need to ignore this handler + // because this event will be handled in `useAnimatedReaction` below + if (layout.value?.layout.height !== input.value?.layout.height) { + return; } + + console.debug("maybeScroll - onChangeText"); + scrollFromCurrentPosition(customHeight); }, [scrollFromCurrentPosition], ); - const onChangeTextHandler = useMemo( () => debounce(onChangeText, 200), [onChangeText], ); + const onSelectionChange = useCallback( + (e: FocusedInputSelectionChangedEvent) => { + "worklet"; + + const lastTarget = lastSelection.value?.target; + + lastSelection.value = e; + + if (e.target !== lastTarget) { + // ignore this event, because "focus changed" event handled in `useSmoothKeyboardHandler` + return; + } + + console.log(e); + + if (e.selection.start.position !== e.selection.end.position) { + console.debug("onSelectionChange - onChangeText"); + + return scrollFromCurrentPosition(e.selection.end.y); + } + + onChangeTextHandler(e.selection.end.y); + }, + [scrollFromCurrentPosition, onChangeTextHandler], + ); useFocusedInputHandler( { - onChangeText: onChangeTextHandler, onSelectionChange: onSelectionChange, }, - [onChangeTextHandler, onSelectionChange], + [onSelectionChange], ); useSmoothKeyboardHandler( @@ -317,6 +350,7 @@ const KeyboardAwareScrollView = forwardRef< if (focusWasChanged && !keyboardWillAppear.value) { // update position on scroll value, so `onEnd` handler // will pick up correct values + console.debug("maybeScroll - onStart"); position.value += maybeScroll(e.height, true); } }, @@ -327,6 +361,7 @@ const KeyboardAwareScrollView = forwardRef< // if the user has set disableScrollOnKeyboardHide, only auto-scroll when the keyboard opens if (!disableScrollOnKeyboardHide || keyboardWillAppear.value) { + console.debug("maybeScroll - onMove"); maybeScroll(e.height); } }, @@ -352,6 +387,7 @@ const KeyboardAwareScrollView = forwardRef< const prevLayout = layout.value; layout.value = input.value; + console.debug("maybeScroll - animated reaction"); scrollPosition.value += maybeScroll(keyboardHeight.value, true); layout.value = prevLayout; }