Skip to content

Commit d352c57

Browse files
committed
fix(android): preserve cursor position when centered TextInput is cleared
On Android, clearing a TextInput with `textAlign: "center"` caused the cursor to jump to the right edge of the field instead of staying centered. The cause was `setText(null)` in `ReactEditText.maybeSetText`, which replaces the underlying `Editable` buffer; recreating the buffer loses the gravity-based caret positioning that `Gravity.CENTER_HORIZONTAL` relies on. Instead of swapping the buffer, remove any composing spans from the existing `Editable` (preserving the original "buggy keyboards don't clear composing text" intent) and clear its contents in place via `Editable.replace(...)`. This mirrors the approach already used by the non-empty branch a few lines below and keeps gravity-based positioning intact, so the cursor stays centered (or right-aligned for RTL/right gravity) after clearing. Adds a Robolectric regression test in `ReactTextInputPropertyTest` that asserts the underlying `Editable` instance is preserved across a clear (the discriminating behavioral difference vs. `setText(null)`). The test was verified to fail against the original buggy code and pass against the fix. Fixes #55457. Made-with: Cursor Made-with:
1 parent ccff70b commit d352c57

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import android.view.MotionEvent
3636
import android.view.View
3737
import android.view.ViewGroup
3838
import android.view.accessibility.AccessibilityNodeInfo
39+
import android.view.inputmethod.BaseInputConnection
3940
import android.view.inputmethod.EditorInfo
4041
import android.view.inputmethod.InputConnection
4142
import android.view.inputmethod.InputMethodManager
@@ -679,9 +680,19 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
679680
disableTextDiffing = true
680681

681682
// On some devices, when the text is cleared, buggy keyboards will not clear the composing
682-
// text so, we have to set text to null, which will clear the currently composing text.
683+
// text. We remove composing spans explicitly and clear the text via replace() on the existing
684+
// Editable rather than setText(null), which would recreate the buffer and cause the cursor
685+
// to lose its gravity-based positioning (e.g. jumping to the right for centered text).
683686
if (reactTextUpdate.text.length == 0) {
684-
text = null
687+
val currentText = editableText
688+
if (currentText != null) {
689+
BaseInputConnection.removeComposingSpans(currentText)
690+
if (currentText.isNotEmpty()) {
691+
currentText.replace(0, currentText.length, "")
692+
}
693+
} else {
694+
text = null
695+
}
685696
} else {
686697
// When we update text, we trigger onChangeText code that will
687698
// try to update state if the wrapper is available. Temporarily disable

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/textinput/ReactTextInputPropertyTest.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import android.text.InputFilter
1717
import android.text.InputFilter.AllCaps
1818
import android.text.InputType
1919
import android.text.Layout
20+
import android.text.SpannableStringBuilder
2021
import android.util.DisplayMetrics
2122
import android.view.Gravity
2223
import android.view.View
@@ -32,6 +33,7 @@ import com.facebook.react.uimanager.DisplayMetricsHolder
3233
import com.facebook.react.uimanager.ReactStylesDiffMap
3334
import com.facebook.react.uimanager.ThemedReactContext
3435
import com.facebook.react.views.text.DefaultStyleValuesUtil.getDefaultTextColorHint
36+
import com.facebook.react.views.text.ReactTextUpdate
3537
import org.assertj.core.api.Assertions.assertThat
3638
import org.junit.Before
3739
import org.junit.Test
@@ -473,6 +475,35 @@ class ReactTextInputPropertyTest {
473475
// endregion
474476
}
475477

478+
@Test
479+
fun testClearingTextPreservesEditableBufferAndGravity() {
480+
// Regression test for #55457. Clearing a centered TextInput must not recreate the
481+
// underlying Editable buffer, otherwise the EditText loses its gravity-based caret
482+
// positioning and the cursor jumps to the right edge.
483+
manager.updateProperties(view, buildStyles("textAlign", "center"))
484+
assertThat(view.gravity and Gravity.HORIZONTAL_GRAVITY_MASK)
485+
.isEqualTo(Gravity.CENTER_HORIZONTAL)
486+
487+
manager.updateExtraData(
488+
view,
489+
ReactTextUpdate(SpannableStringBuilder("hello"), 0, Gravity.CENTER_HORIZONTAL, 0, 0))
490+
val bufferBeforeClear = view.editableText
491+
assertThat(view.text.toString()).isEqualTo("hello")
492+
assertThat(bufferBeforeClear).isNotNull()
493+
494+
manager.updateExtraData(
495+
view, ReactTextUpdate(SpannableStringBuilder(""), 0, Gravity.CENTER_HORIZONTAL, 0, 0))
496+
497+
// Behavioral guarantee of the fix: the Editable instance is reused via replace(),
498+
// not swapped out via setText(null). This is what keeps the cursor centered.
499+
assertThat(view.editableText).isSameAs(bufferBeforeClear)
500+
assertThat(view.text.toString()).isEmpty()
501+
assertThat(view.gravity and Gravity.HORIZONTAL_GRAVITY_MASK)
502+
.isEqualTo(Gravity.CENTER_HORIZONTAL)
503+
assertThat(view.selectionStart).isEqualTo(0)
504+
assertThat(view.selectionEnd).isEqualTo(0)
505+
}
506+
476507
@Test
477508
fun testMaxLength() {
478509
val filters = arrayOf<InputFilter>(AllCaps())

0 commit comments

Comments
 (0)