diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index a0f2152677e3..221aeca94937 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -36,6 +36,7 @@ import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityNodeInfo +import android.view.inputmethod.BaseInputConnection import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import android.view.inputmethod.InputMethodManager @@ -679,9 +680,19 @@ public open class ReactEditText public constructor(context: Context) : AppCompat disableTextDiffing = true // On some devices, when the text is cleared, buggy keyboards will not clear the composing - // text so, we have to set text to null, which will clear the currently composing text. + // text. We remove composing spans explicitly and clear the text via replace() on the existing + // Editable rather than setText(null), which would recreate the buffer and cause the cursor + // to lose its gravity-based positioning (e.g. jumping to the right for centered text). if (reactTextUpdate.text.length == 0) { - text = null + val currentText = editableText + if (currentText != null) { + BaseInputConnection.removeComposingSpans(currentText) + if (currentText.isNotEmpty()) { + currentText.replace(0, currentText.length, "") + } + } else { + text = null + } } else { // When we update text, we trigger onChangeText code that will // try to update state if the wrapper is available. Temporarily disable diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt index e1fe5c6b27c1..656e4d0ca7b9 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt @@ -17,6 +17,7 @@ import android.text.InputFilter import android.text.InputFilter.AllCaps import android.text.InputType import android.text.Layout +import android.text.SpannableStringBuilder import android.util.DisplayMetrics import android.view.Gravity import android.view.View @@ -32,6 +33,7 @@ import com.facebook.react.uimanager.DisplayMetricsHolder import com.facebook.react.uimanager.ReactStylesDiffMap import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.views.text.DefaultStyleValuesUtil.getDefaultTextColorHint +import com.facebook.react.views.text.ReactTextUpdate import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -473,6 +475,35 @@ class ReactTextInputPropertyTest { // endregion } + @Test + fun testClearingTextPreservesEditableBufferAndGravity() { + // Regression test for #55457. Clearing a centered TextInput must not recreate the + // underlying Editable buffer, otherwise the EditText loses its gravity-based caret + // positioning and the cursor jumps to the right edge. + manager.updateProperties(view, buildStyles("textAlign", "center")) + assertThat(view.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) + .isEqualTo(Gravity.CENTER_HORIZONTAL) + + manager.updateExtraData( + view, + ReactTextUpdate(SpannableStringBuilder("hello"), 0, Gravity.CENTER_HORIZONTAL, 0, 0)) + val bufferBeforeClear = view.editableText + assertThat(view.text.toString()).isEqualTo("hello") + assertThat(bufferBeforeClear).isNotNull() + + manager.updateExtraData( + view, ReactTextUpdate(SpannableStringBuilder(""), 0, Gravity.CENTER_HORIZONTAL, 0, 0)) + + // Behavioral guarantee of the fix: the Editable instance is reused via replace(), + // not swapped out via setText(null). This is what keeps the cursor centered. + assertThat(view.editableText).isSameAs(bufferBeforeClear) + assertThat(view.text.toString()).isEmpty() + assertThat(view.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) + .isEqualTo(Gravity.CENTER_HORIZONTAL) + assertThat(view.selectionStart).isEqualTo(0) + assertThat(view.selectionEnd).isEqualTo(0) + } + @Test fun testMaxLength() { val filters = arrayOf(AllCaps())