Skip to content

fix: report the iOS caret y of single-line inputs from the field's top - #1691

Open
ErickValdivia wants to merge 1 commit into
kirillzyusko:mainfrom
ErickValdivia:fix/ios-single-line-caret-y
Open

ErickValdivia wants to merge 1 commit into
kirillzyusko:mainfrom
ErickValdivia:fix/ios-single-line-caret-y

Conversation

@ErickValdivia

Copy link
Copy Markdown

📜 Description

Report the caret y of a single-line TextInput on iOS from the field's top, as Android does, so KeyboardAwareScrollView scrolls a padded single-line input fully above the keyboard.

💡 Motivation and Context

textSelection(in:) sends caretRect(for:) as it is. That rect is in textInputView's coordinates, and for a UITextField that's the inner text area, which React Native places inside the field's paddingTop. So the caret of a single-line input came out one top padding higher than it is.

KeyboardAwareScrollView adds selection.end.y to the input's absoluteY (updateLayoutFromSelection) and scrolls until that point clears the keyboard plus bottomOffset. With the caret reported too high, it stopped one top padding short: with paddingTop: 30 and bottomOffset: 20, the typed text ended up under the keyboard.

Android already measures from the field's top: KeyboardControllerSelectionWatcher adds paddingTop (and the gravity offset) to the line's top and bottom.

  • Only UITextField changes. caretYOffset returns 0 for a UITextView, so multiline inputs keep reporting what they do today.
  • x is unchanged, matching Android, where x comes from getPrimaryHorizontal without the horizontal padding.

📢 Changelog

iOS

  • report the caret y of single-line inputs (UITextField) from the field's top, adding the offset of textInputView inside the field

🤔 How Has This Been Tested?

  • example app (Paper, RN 0.81.4), iPhone 16 simulator, iOS 18.6, on-screen keyboard. The "Aware scroll view" screen temporarily replaced with single-line inputs (height: 70, paddingTop: 30, bottomOffset={20}; snippet below), with an on-screen readout of selection.end.y from useFocusedInputHandler. Focused #5 and typed "hello":
    • before: selection.end.y = 27.3 and the text sits under the keyboard
    • after: selection.end.y = 57.3 (+30, the top padding) and the text stays above the keyboard
  • A production app on the New Architecture (RN 0.86, 1.22.7 with this change as a patch): a single-line input with paddingTop: 18 went from 35.3 to 53.3 and now clears the keyboard. In a @gorhom/bottom-sheet with KeyboardAwareScrollView, a single-line input now scrolls by its top padding more (68pt instead of 50pt) and stays visible; multiline inputs don't change.
  • Not added: a native unit test. KCTextInputCompositeDelegate.swift isn't compiled into KeyboardControllerNativeTests; happy to add it there with a test if you'd like.
Repro screen (not part of this PR)
import React from "react";
import { StyleSheet, TextInput } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";

export default function AwareScrollView() {
  return (
    <KeyboardAwareScrollView bottomOffset={20} contentContainerStyle={styles.content}>
      {new Array(8).fill(0).map((_, i) => (
        <TextInput key={i} placeholder={`Single-line #${i} (paddingTop: 30)`} style={styles.input} />
      ))}
    </KeyboardAwareScrollView>
  );
}

const styles = StyleSheet.create({
  content: { padding: 16 },
  input: { height: 70, paddingTop: 30, paddingHorizontal: 12, marginBottom: 24, borderWidth: 2, borderRadius: 10 },
});

📸 Screenshots (if appropriate):

Before (main): the caret is reported at 27.3 and "hello" sits under the keyboard:

before.mp4

After: 57.3, and "hello" stays above the keyboard:

after.mp4

📝 Checklist

  • CI successfully passed
  • I added new mocks and corresponding unit-tests if library API was changed (the API didn't change)

@ErickValdivia
ErickValdivia marked this pull request as ready for review October 16, 2026 19:36
@github-actions

Copy link
Copy Markdown
Contributor

📊 Package size report

Current size Target Size Difference
347032 bytes 346819 bytes 213 bytes 📈

@github-actions

Copy link
Copy Markdown
Contributor
  1. Incorrect y-coordinate calculation in textSelection function for end position
    Why: The end position's y-coordinate is being adjusted by adding caretRectEnd.size.height and yOffset, which might double the offset if caretYOffset already accounts for it. This could lead to incorrect caret position reporting.
    Fix: Review the yOffset calculation and adjust the end position's y-coordinate calculation to ensure it's accurate without double-counting the offset.

  2. Potential crash when textInput is not a UITextField
    Why: The caretYOffset function assumes textInput is a UITextField, which might not always be the case. If textInput is a UITextView, it could cause a runtime crash when trying to cast it to UITextField.
    Fix: Modify caretYOffset to handle both UITextField and UITextView appropriately or add a check to ensure the cast is valid before accessing properties specific to UITextField.

@ErickValdivia

Copy link
Copy Markdown
Author

Both are false positives, so no change:

  1. yOffset is only the origin of textInputView inside the field, so adding it to the caret's bottom (origin.y + size.height) moves that bottom into the field's coordinates; it's the same as textField.textInputView.convert(caretRectEnd, to: textField).maxY. Start and end both move by exactly the top padding, and the caret height doesn't change (example app: end.y 27.3 → 57.3 with paddingTop: 30).
  2. as? is a conditional cast: for a UITextView or any other UITextInput it returns nil, the guard returns 0, and multiline inputs keep reporting what they do today. It can't crash.

@kirillzyusko

kirillzyusko commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Yes @ErickValdivia it's 14m model, so it's not very smart 🤓

Main thing is e2e tests here, they matter more 😅

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants