From 7a2cf6316f6532475505add35b8ff663044aa0a0 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 16 Oct 2026 00:33:30 +0300 Subject: [PATCH] Keep an email field's cursor hidden on Xiaomi Android 10 when caretHidden changes setCaretHidden left an email field alone only when its staged input type equalled TYPE_TEXT_VARIATION_EMAIL_ADDRESS, which an email field's type, carrying TYPE_CLASS_TEXT too, never does, so any caretHidden update showed the cursor the #27204 workaround hides. The cursor is now set once per update, in onAfterUpdateTransaction after the input type is committed, from caretHidden and the committed type's class and variation. --- .../react/views/textinput/ReactEditText.kt | 1 + .../views/textinput/ReactTextInputManager.kt | 32 ++++++---------- .../textinput/ReactTextInputPropertyTest.kt | 38 +++++++++++++++++++ 3 files changed, 51 insertions(+), 20 deletions(-) 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 754f10026624..a7196107d51f 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 @@ -123,6 +123,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat public var stagedInputType: Int internal var stagedAutoCapitalize: Int = 0 + internal var stagedCaretHidden: Boolean = false public var submitBehavior: String? = null public var dragAndDropFilter: List? = null diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt index 82e5abd361bd..258bcbe3a87f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt @@ -479,15 +479,10 @@ public open class ReactTextInputManager public constructor() : } catch (_: IllegalAccessException) {} } + // Deferred to onAfterUpdateTransaction() so the cursor follows the resolved keyboard type. @ReactProp(name = "caretHidden", defaultBoolean = false) public fun setCaretHidden(view: ReactEditText, caretHidden: Boolean) { - if ( - view.stagedInputType == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS && - shouldHideCursorForEmailTextInput() - ) { - return - } - view.isCursorVisible = !caretHidden + view.stagedCaretHidden = caretHidden } @ReactProp(name = "contextMenuHidden", defaultBoolean = false) @@ -744,15 +739,6 @@ public open class ReactTextInputManager public constructor() : flagsToSet = INPUT_TYPE_KEYBOARD_DECIMAL_PAD } else if (KEYBOARD_TYPE_EMAIL_ADDRESS.equals(keyboardType, ignoreCase = true)) { flagsToSet = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS or InputType.TYPE_CLASS_TEXT - - // Set cursor's visibility to False to fix a crash on some Xiaomi devices with Android Q. This - // crash happens when focusing on a email EditText, during which a prompt will be triggered - // but - // the system fail to locate it properly. Here is an example post discussing about this - // issue: https://github.com/facebook/react-native/issues/27204 - if (shouldHideCursorForEmailTextInput()) { - view.isCursorVisible = false - } } else if (KEYBOARD_TYPE_PHONE_PAD.equals(keyboardType, ignoreCase = true)) { flagsToSet = InputType.TYPE_CLASS_PHONE } else if (KEYBOARD_TYPE_VISIBLE_PASSWORD.equals(keyboardType, ignoreCase = true)) { @@ -907,6 +893,8 @@ public open class ReactTextInputManager public constructor() : super.onAfterUpdateTransaction(view) reconcileAutoCapitalize(view) view.commitStagedInputType() + view.isCursorVisible = + !view.stagedCaretHidden && !shouldHideCursorForEmailTextInput(view.inputType) // setInputType() clears paint-level font variation settings, so update the typeface last to // reapply any variation axes after the staged input type is committed. view.maybeUpdateTypeface() @@ -1163,10 +1151,14 @@ public open class ReactTextInputManager public constructor() : private val DRAWABLE_HANDLE_FIELDS = arrayOf("mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter") - private fun shouldHideCursorForEmailTextInput(): Boolean { - val manufacturer = Build.MANUFACTURER.lowercase() - return (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && manufacturer.contains("xiaomi")) - } + // Some Xiaomi devices on Android 10 crash drawing an email field's cursor while showing their + // email suggestion popup: https://github.com/facebook/react-native/issues/27204 + private fun shouldHideCursorForEmailTextInput(inputType: Int): Boolean = + (inputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT && + (inputType and InputType.TYPE_MASK_VARIATION) == + InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS && + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && + Build.MANUFACTURER.lowercase().contains("xiaomi") private const val IME_ACTION_ID = 0x670 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 82ec014eb89e..fcd63603d620 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 @@ -48,6 +48,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowBuild /** Verify {@link EditText} view property being applied properly by {@link ReactTextInputManager} */ @RunWith(RobolectricTestRunner::class) @@ -377,6 +379,42 @@ class ReactTextInputPropertyTest { assertThat(view.inputType and generalKeyboardTypeFlags).isEqualTo(emailTypeFlags) } + @Test + fun testCaretHidden() { + manager.updateProperties(view, buildStyles("keyboardType", "email-address")) + assertThat(view.isCursorVisible).isTrue + + manager.updateProperties(view, buildStyles("caretHidden", true)) + assertThat(view.isCursorVisible).isFalse + + manager.updateProperties(view, buildStyles("caretHidden", false)) + assertThat(view.isCursorVisible).isTrue + } + + @Test + @Config(sdk = [Build.VERSION_CODES.Q]) + fun testEmailCursorStaysHiddenOnXiaomiAndroid10WhenCaretHiddenIsUpdated() { + ShadowBuild.setManufacturer("Xiaomi") + + manager.updateProperties(view, buildStyles("keyboardType", "email-address")) + assertThat(view.isCursorVisible).isFalse + + manager.updateProperties(view, buildStyles("caretHidden", false)) + assertThat(view.isCursorVisible).isFalse + } + + @Test + @Config(sdk = [Build.VERSION_CODES.Q]) + fun testCaretHiddenStillAppliesToOtherFieldsOnXiaomiAndroid10() { + ShadowBuild.setManufacturer("Xiaomi") + + manager.updateProperties(view, buildStyles("caretHidden", true)) + assertThat(view.isCursorVisible).isFalse + + manager.updateProperties(view, buildStyles("caretHidden", false)) + assertThat(view.isCursorVisible).isTrue + } + @Test fun testKeyboardTypeUrl() { manager.updateProperties(view, buildStyles("keyboardType", "url"))