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..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,6 +117,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var listeners: CopyOnWriteArrayList? public var stagedInputType: Int + internal var stagedAutoCapitalize: Int = 0 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 21fbfb4d06d9..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 @@ -718,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") @@ -882,6 +884,7 @@ public open class ReactTextInputManager public constructor() : override fun onAfterUpdateTransaction(view: ReactEditText) { super.onAfterUpdateTransaction(view) view.maybeUpdateTypeface() + reconcileAutoCapitalize(view) view.commitStagedInputType() } @@ -1129,6 +1132,27 @@ 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 + val inputClass = view.stagedInputType and InputType.TYPE_MASK_CLASS + + // 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 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 1fc4004dc540..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 @@ -125,6 +125,65 @@ 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) + + manager.updateProperties(view, buildStyles("keyboardType", "numeric")) + assertThat(view.inputType and numericTypeFlags).isEqualTo(numericTypeFlags) + + 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 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())