From f13ea2fd64591e2aa26cb253eb90795249c8e6ef Mon Sep 17 00:00:00 2001 From: Luke Harvey Date: Thu, 26 Feb 2026 11:45:46 -0500 Subject: [PATCH 1/3] Fix autoCapitalize bitmask collision with numeric inputType flags --- .../views/textinput/ReactTextInputManager.kt | 8 +++++++ .../textinput/ReactTextInputPropertyTest.kt | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) 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 21fbfb4d06d9..fb4cc29c6c57 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 @@ -704,6 +704,14 @@ public open class ReactTextInputManager public constructor() : // See T46146267 @ReactProp(name = "autoCapitalize") public fun setAutoCapitalize(view: ReactEditText, autoCapitalize: Dynamic) { + // Autocapitalize is meaningless for number inputs. AUTOCAPITALIZE_FLAGS (0x7000) + // overlaps TYPE_NUMBER_FLAG_SIGNED (0x1000) and TYPE_NUMBER_FLAG_DECIMAL (0x2000), + // so applying the mask strips signed/decimal flags, corrupting the inputType and + // causing setInputType() to restart the IME on every prop update. + if ((view.stagedInputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER) { + return + } + var autoCapitalizeValue = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES if (autoCapitalize.type == ReadableType.Number) { 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 1fc4004dc540..aa9357a3d62b 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 @@ -125,6 +125,28 @@ class ReactTextInputPropertyTest { assertThat(view.inputType and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS).isZero } + @Test + fun testAutoCapitalizeDoesNotStripNumericFlags() { + val numericTypeFlags = + (InputType.TYPE_CLASS_NUMBER or + InputType.TYPE_NUMBER_FLAG_DECIMAL or + InputType.TYPE_NUMBER_FLAG_SIGNED) + + // Set up a numeric keyboard type first + manager.updateProperties(view, buildStyles("keyboardType", "numeric")) + assertThat(view.inputType and numericTypeFlags).isEqualTo(numericTypeFlags) + + // Simulate a Fabric re-render applying autoCapitalize to the numeric input. + // AUTOCAPITALIZE_FLAGS (0x7000) overlaps TYPE_NUMBER_FLAG_SIGNED (0x1000) and + // TYPE_NUMBER_FLAG_DECIMAL (0x2000) — this must not strip those flags. + manager.updateProperties( + view, + buildStyles("autoCapitalize", InputType.TYPE_TEXT_FLAG_CAP_SENTENCES), + ) + assertThat(view.inputType and InputType.TYPE_NUMBER_FLAG_SIGNED).isNotZero + assertThat(view.inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL).isNotZero + } + @Test fun testPlaceholder() { manager.updateProperties(view, buildStyles()) From 5f1affa1c42d87a218252482b31c9c7f217b3904 Mon Sep 17 00:00:00 2001 From: Luke Harvey Date: Fri, 27 Feb 2026 19:28:20 -0500 Subject: [PATCH 2/3] Reconcile autoCapitalize in onAfterUpdateTransaction --- .../react/views/textinput/ReactEditText.kt | 2 + .../views/textinput/ReactTextInputManager.kt | 32 +++++++++---- .../textinput/ReactTextInputPropertyTest.kt | 45 +++++++++++++++++-- 3 files changed, 66 insertions(+), 13 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 ff410da5caed..67986e4a718a 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 @@ -117,6 +117,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var listeners: CopyOnWriteArrayList? public var stagedInputType: Int + internal var stagedAutoCapitalize: Int = UNSET_AUTO_CAPITALIZE public var submitBehavior: String? = null public var dragAndDropFilter: List? = null @@ -1222,6 +1223,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat public companion object { public val DEBUG_MODE: Boolean = ReactBuildConfig.DEBUG && false + internal const val UNSET_AUTO_CAPITALIZE: Int = -1 private val keyListener: KeyListener = QwertyKeyListener.getInstanceForFullKeyboard() 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 fb4cc29c6c57..0ad2eead4deb 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 @@ -704,14 +704,6 @@ public open class ReactTextInputManager public constructor() : // See T46146267 @ReactProp(name = "autoCapitalize") public fun setAutoCapitalize(view: ReactEditText, autoCapitalize: Dynamic) { - // Autocapitalize is meaningless for number inputs. AUTOCAPITALIZE_FLAGS (0x7000) - // overlaps TYPE_NUMBER_FLAG_SIGNED (0x1000) and TYPE_NUMBER_FLAG_DECIMAL (0x2000), - // so applying the mask strips signed/decimal flags, corrupting the inputType and - // causing setInputType() to restart the IME on every prop update. - if ((view.stagedInputType and InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER) { - return - } - var autoCapitalizeValue = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES if (autoCapitalize.type == ReadableType.Number) { @@ -726,7 +718,9 @@ public open class ReactTextInputManager public constructor() : } } - updateStagedInputTypeFlag(view, AUTOCAPITALIZE_FLAGS, autoCapitalizeValue) + // Deferred to onAfterUpdateTransaction() so we can reconcile with the resolved + // keyboard type — AUTOCAPITALIZE_FLAGS collides with numeric inputType flags. + view.stagedAutoCapitalize = autoCapitalizeValue } @ReactProp(name = "keyboardType") @@ -890,6 +884,7 @@ public open class ReactTextInputManager public constructor() : override fun onAfterUpdateTransaction(view: ReactEditText) { super.onAfterUpdateTransaction(view) view.maybeUpdateTypeface() + reconcileAutoCapitalize(view) view.commitStagedInputType() } @@ -1137,6 +1132,25 @@ public open class ReactTextInputManager public constructor() : private const val IME_ACTION_ID = 0x670 + // AUTOCAPITALIZE_FLAGS (0x7000) shares bit positions with TYPE_NUMBER_FLAG_SIGNED + // (0x1000) and TYPE_NUMBER_FLAG_DECIMAL (0x2000). We apply autocapitalize here + // after all props are set so the resolved input class determines whether the + // flags are meaningful. + private fun reconcileAutoCapitalize(view: ReactEditText) { + val autoCapValue = view.stagedAutoCapitalize + if (autoCapValue == ReactEditText.UNSET_AUTO_CAPITALIZE) return + + val inputClass = view.stagedInputType and InputType.TYPE_MASK_CLASS + if (inputClass == InputType.TYPE_CLASS_TEXT) { + updateStagedInputTypeFlag(view, AUTOCAPITALIZE_FLAGS, autoCapValue) + } else { + // Only strip 0x4000 (CAP_SENTENCES) — 0x1000/0x2000 are valid numeric flags + // (SIGNED/DECIMAL) and must not be cleared. + view.stagedInputType = + view.stagedInputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv() + } + } + // Sets the correct password type, since numeric and text passwords have different types private fun checkPasswordType(view: ReactEditText) { if ( 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 aa9357a3d62b..844e237e69cb 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 @@ -132,13 +132,9 @@ class ReactTextInputPropertyTest { InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED) - // Set up a numeric keyboard type first manager.updateProperties(view, buildStyles("keyboardType", "numeric")) assertThat(view.inputType and numericTypeFlags).isEqualTo(numericTypeFlags) - // Simulate a Fabric re-render applying autoCapitalize to the numeric input. - // AUTOCAPITALIZE_FLAGS (0x7000) overlaps TYPE_NUMBER_FLAG_SIGNED (0x1000) and - // TYPE_NUMBER_FLAG_DECIMAL (0x2000) — this must not strip those flags. manager.updateProperties( view, buildStyles("autoCapitalize", InputType.TYPE_TEXT_FLAG_CAP_SENTENCES), @@ -147,6 +143,47 @@ class ReactTextInputPropertyTest { assertThat(view.inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL).isNotZero } + @Test + fun testAutoCapitalizeAndNumericKeyboardInSameTransaction() { + val numericTypeFlags = + (InputType.TYPE_CLASS_NUMBER or + InputType.TYPE_NUMBER_FLAG_DECIMAL or + InputType.TYPE_NUMBER_FLAG_SIGNED) + + manager.updateProperties( + view, + buildStyles( + "autoCapitalize", + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES, + "keyboardType", + "numeric", + ), + ) + assertThat(view.inputType and numericTypeFlags).isEqualTo(numericTypeFlags) + assertThat(view.inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isZero + } + + @Test + fun testAutoCapitalizeReappliesWhenKeyboardTypeChangesFromNumericToText() { + // CAP_SENTENCES (0x4000) doesn't share a bit position with any numeric flag, + // unlike CAP_WORDS (0x2000) / CAP_CHARACTERS (0x1000). + manager.updateProperties( + view, + buildStyles( + "autoCapitalize", + InputType.TYPE_TEXT_FLAG_CAP_SENTENCES, + "keyboardType", + "numeric", + ), + ) + assertThat(view.inputType and InputType.TYPE_MASK_CLASS).isEqualTo(InputType.TYPE_CLASS_NUMBER) + assertThat(view.inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isZero + + manager.updateProperties(view, buildStyles("keyboardType", "default")) + assertThat(view.inputType and InputType.TYPE_MASK_CLASS).isEqualTo(InputType.TYPE_CLASS_TEXT) + assertThat(view.inputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES).isNotZero + } + @Test fun testPlaceholder() { manager.updateProperties(view, buildStyles()) From c8eaf1d77042070134fbe5e5c7c7fc6ef0dbc594 Mon Sep 17 00:00:00 2001 From: Luke Harvey Date: Sat, 28 Feb 2026 20:49:51 -0500 Subject: [PATCH 3/3] Skip reconcileAutoCapitalize when flags are unchanged --- .../react/views/textinput/ReactEditText.kt | 3 +-- .../views/textinput/ReactTextInputManager.kt | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 11 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 67986e4a718a..f1dffff85527 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 @@ -117,7 +117,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var listeners: CopyOnWriteArrayList? public var stagedInputType: Int - internal var stagedAutoCapitalize: Int = UNSET_AUTO_CAPITALIZE + internal var stagedAutoCapitalize: Int = 0 public var submitBehavior: String? = null public var dragAndDropFilter: List? = null @@ -1223,7 +1223,6 @@ public open class ReactEditText public constructor(context: Context) : AppCompat public companion object { public val DEBUG_MODE: Boolean = ReactBuildConfig.DEBUG && false - internal const val UNSET_AUTO_CAPITALIZE: Int = -1 private val keyListener: KeyListener = QwertyKeyListener.getInstanceForFullKeyboard() 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 0ad2eead4deb..faaf625eab3e 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 @@ -1138,17 +1138,19 @@ public open class ReactTextInputManager public constructor() : // flags are meaningful. private fun reconcileAutoCapitalize(view: ReactEditText) { val autoCapValue = view.stagedAutoCapitalize - if (autoCapValue == ReactEditText.UNSET_AUTO_CAPITALIZE) return - val inputClass = view.stagedInputType and InputType.TYPE_MASK_CLASS - if (inputClass == InputType.TYPE_CLASS_TEXT) { - updateStagedInputTypeFlag(view, AUTOCAPITALIZE_FLAGS, autoCapValue) - } else { - // Only strip 0x4000 (CAP_SENTENCES) — 0x1000/0x2000 are valid numeric flags - // (SIGNED/DECIMAL) and must not be cleared. - view.stagedInputType = + + // Only strip 0x4000 (CAP_SENTENCES) for non-text classes — 0x1000/0x2000 are + // valid numeric flags (SIGNED/DECIMAL) and must not be cleared. + val reconciled = + if (inputClass == InputType.TYPE_CLASS_TEXT) { + (view.stagedInputType and AUTOCAPITALIZE_FLAGS.inv()) or autoCapValue + } else { view.stagedInputType and InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv() - } + } + + if (view.stagedInputType == reconciled) return + view.stagedInputType = reconciled } // Sets the correct password type, since numeric and text passwords have different types