diff --git a/src/components/KeyboardAwareScrollView/__fixtures__/mocks.tsx b/src/components/KeyboardAwareScrollView/__fixtures__/mocks.tsx index 1f1ca690ab..e15b80292a 100644 --- a/src/components/KeyboardAwareScrollView/__fixtures__/mocks.tsx +++ b/src/components/KeyboardAwareScrollView/__fixtures__/mocks.tsx @@ -13,6 +13,7 @@ // --------------------------------------------------------------------------- const MOCK_SCREEN_H = 928; const MOCK_SV = 1469; +const MOCK_SV_PAGE_Y = 116; const mockInterpolateFn = ( value: number, @@ -89,7 +90,7 @@ jest.mock("../../../utils/findNodeHandle", () => ({ jest.mock("../../../bindings", () => ({ KeyboardControllerNative: { - viewPositionInWindow: jest.fn().mockResolvedValue({ y: 0 }), + viewPositionInWindow: jest.fn().mockResolvedValue({ y: MOCK_SV_PAGE_Y }), }, })); diff --git a/src/components/KeyboardAwareScrollView/__fixtures__/testUtils.tsx b/src/components/KeyboardAwareScrollView/__fixtures__/testUtils.tsx index 13e0dca5fb..a729c89d6c 100644 --- a/src/components/KeyboardAwareScrollView/__fixtures__/testUtils.tsx +++ b/src/components/KeyboardAwareScrollView/__fixtures__/testUtils.tsx @@ -17,6 +17,7 @@ export const MOCK_SCREEN_HEIGHT = 928; export const KEYBOARD_HEIGHT = 312; export const BOTTOM_OFFSET = 62; export const MOCK_SV_TARGET = 1469; +export const MOCK_SV_PAGE_Y = 116; export const INPUT_TARGET_A = 1373; export const INPUT_TARGET_B = 1395; diff --git a/src/components/KeyboardAwareScrollView/__tests__/keyboardSizeChange.spec.tsx b/src/components/KeyboardAwareScrollView/__tests__/keyboardSizeChange.spec.tsx new file mode 100644 index 0000000000..cdef478d90 --- /dev/null +++ b/src/components/KeyboardAwareScrollView/__tests__/keyboardSizeChange.spec.tsx @@ -0,0 +1,103 @@ +import "../__fixtures__/mocks"; + +import { + INPUT_LAYOUT_B, + INPUT_TARGET_B, + KEYBOARD_HEIGHT, + inputEvent, + kbEvent, + lastScrollToY, + mockInput, + mockKeyboardHandlers, + mockOffset, + mockScrollTo, + mockSelectionHandler, + renderKeyboardAwareScrollView, + reset, + selectionEvent, +} from "../__fixtures__/testUtils"; + +beforeEach(() => { + reset(); +}); + +const EMOJI_KEYBOARD_HEIGHT = 388; + +describe("KeyboardAwareScrollView — keyboard size change (emoji toggle)", () => { + // Input B: absoluteY=695.67, selectionHeight=47 + // point = 695.67 + 47 = 742.67 + // + // With text keyboard (312): + // visibleRect = 928 - 312 = 616 + // relativeScrollTo = 312 - (928 - 742.67) + 62 = 188.67 + // + // With emoji keyboard (388): + // visibleRect = 928 - 388 = 540 + // Corrected absoluteY = 695.67 - 188.67 = 507 + // point = 507 + 47 = 554 + // relativeScrollTo = 388 - (928 - 554) + 62 = 76 + // targetScrollY = 76 + 0 (scrollBeforeKeyboardMovement) = ... but + // interpolation base is scrollPosition=188.67, so: + // targetScrollY = 76 + 188.67 = 264.67 + + it("should not accumulate scroll when switching emoji ↔ text keyboard", async () => { + await renderKeyboardAwareScrollView(); + mockInput.value = inputEvent(INPUT_TARGET_B, INPUT_LAYOUT_B); + + // ---- First focus — keyboard opens ---- + mockSelectionHandler.current(selectionEvent(INPUT_TARGET_B)); + mockKeyboardHandlers.current.onStart( + kbEvent(KEYBOARD_HEIGHT, INPUT_TARGET_B), + ); + mockKeyboardHandlers.current.onMove( + kbEvent(KEYBOARD_HEIGHT, INPUT_TARGET_B), + ); + mockKeyboardHandlers.current.onEnd( + kbEvent(KEYBOARD_HEIGHT, INPUT_TARGET_B), + ); + mockOffset.value = 188.67; + + // ---- Switch to emoji keyboard (larger) ---- + mockScrollTo.mockClear(); + + mockKeyboardHandlers.current.onStart({ + height: EMOJI_KEYBOARD_HEIGHT, + target: INPUT_TARGET_B, + duration: 0, + progress: 1, + }); + mockKeyboardHandlers.current.onEnd({ + height: EMOJI_KEYBOARD_HEIGHT, + target: INPUT_TARGET_B, + duration: 0, + progress: 1, + }); + + const scrollAfterEmoji = lastScrollToY(); + + // Should scroll a bit more (keyboard grew), but NOT double the amount + expect(scrollAfterEmoji).toBeDefined(); + expect(scrollAfterEmoji!).toBeLessThan(300); + + // Simulate the scroll settling + mockOffset.value = scrollAfterEmoji!; + + // ---- Switch back to text keyboard (smaller) ---- + mockScrollTo.mockClear(); + + mockKeyboardHandlers.current.onStart( + kbEvent(KEYBOARD_HEIGHT, INPUT_TARGET_B), + ); + mockKeyboardHandlers.current.onEnd( + kbEvent(KEYBOARD_HEIGHT, INPUT_TARGET_B), + ); + + const scrollAfterText = lastScrollToY(); + + // Key assertion: position should NOT exceed the emoji keyboard scroll + // (i.e. no accumulation happening) + expect(scrollAfterText ?? mockOffset.value).toBeLessThan( + scrollAfterEmoji! + 10, + ); + }); +}); diff --git a/src/components/KeyboardAwareScrollView/index.tsx b/src/components/KeyboardAwareScrollView/index.tsx index da5bf242a4..c92d79b074 100644 --- a/src/components/KeyboardAwareScrollView/index.tsx +++ b/src/components/KeyboardAwareScrollView/index.tsx @@ -379,9 +379,8 @@ const KeyboardAwareScrollView = forwardRef< keyboardWillAppear.value = e.height > 0 && keyboardHeight.value === 0; const keyboardWillHide = e.height === 0; - const focusWasChanged = - (tag.value !== e.target && e.target !== -1) || - keyboardWillChangeSize; + const actualFocusChanged = tag.value !== e.target && e.target !== -1; + const focusWasChanged = actualFocusChanged || keyboardWillChangeSize; if (keyboardWillChangeSize) { initialKeyboardSize.value = keyboardHeight.value; @@ -433,8 +432,26 @@ const KeyboardAwareScrollView = forwardRef< } // save current scroll position - when keyboard will hide we'll reuse - // this value to achieve smooth hide effect - scrollBeforeKeyboardMovement.value = position.value; + // this value to achieve smooth hide effect (only for actual focus change + // keyboard resize handled below) + if (actualFocusChanged) { + scrollBeforeKeyboardMovement.value = position.value; + } + } + + // compute the actual on-screen position from the input's content-relative Y, + // current scroll offset, and ScrollView's position (case for keyboard resize) + if (keyboardWillChangeSize && !actualFocusChanged && layout.value) { + layout.value = { + ...layout.value, + layout: { + ...layout.value.layout, + absoluteY: + layout.value.layout.y - + position.value + + scrollViewPageY.value, + }, + }; } if (focusWasChanged && !keyboardWillAppear.value) {