Skip to content

Commit 2ed43b6

Browse files
Compose Android keyboardType and secureTextEntry into one input type
keyboardType and secureTextEntry both write the class and variation of the staged input type, each with bit edits applied in the order the props arrive. A variation is a value within its class, not a set of flags: 0x10 is TYPE_TEXT_VARIATION_URI in the text class and TYPE_NUMBER_VARIATION_PASSWORD in the number class. Depending on that order: - secureTextEntry={false} clears the url and visible-password variations. - email-address with secureTextEntry commits TYPE_TEXT_VARIATION_WEB_EDIT_TEXT, which is not masked. - secureTextEntry set after autoCapitalize="words" or "characters" commits text + URI, which is not masked, because checkPasswordType reads the capitalization flags as the number flags they share bits with. - Changing keyboardType keeps the previous variation (url then email-address commits EMAIL_SUBJECT) and the previous number flags. - phone-pad with secureTextEntry commits a variation the phone class does not define. Both setters now only stage their value, and onAfterUpdateTransaction resolves the class and variation once, before reconcileAutoCapitalize reads the class, the way autoCapitalize is already deferred. A secure field takes the password variation of its class, and a class without one keeps its keyboard.
1 parent e0773a7 commit 2ed43b6

3 files changed

Lines changed: 166 additions & 28 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ public open class ReactEditText public constructor(context: Context) : AppCompat
123123

124124
public var stagedInputType: Int
125125
internal var stagedAutoCapitalize: Int = 0
126+
internal var stagedKeyboardInputType: Int = InputType.TYPE_CLASS_TEXT
127+
internal var stagedSecureTextEntry: Boolean = false
126128
public var submitBehavior: String? = null
127129
public var dragAndDropFilter: List<String>? = null
128130

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

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -696,17 +696,8 @@ public open class ReactTextInputManager public constructor() :
696696

697697
@ReactProp(name = "secureTextEntry", defaultBoolean = false)
698698
public fun setSecureTextEntry(view: ReactEditText, password: Boolean) {
699-
updateStagedInputTypeFlag(
700-
view = view,
701-
flagsToUnset =
702-
if (password) {
703-
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
704-
} else {
705-
InputType.TYPE_NUMBER_VARIATION_PASSWORD or InputType.TYPE_TEXT_VARIATION_PASSWORD
706-
},
707-
flagsToSet = if (password) InputType.TYPE_TEXT_VARIATION_PASSWORD else 0,
708-
)
709-
checkPasswordType(view)
699+
// Deferred to onAfterUpdateTransaction(), where it is applied to the keyboard type.
700+
view.stagedSecureTextEntry = password
710701
}
711702

712703
// This prop temporarily takes both numbers and strings.
@@ -756,15 +747,13 @@ public open class ReactTextInputManager public constructor() :
756747
} else if (KEYBOARD_TYPE_PHONE_PAD.equals(keyboardType, ignoreCase = true)) {
757748
flagsToSet = InputType.TYPE_CLASS_PHONE
758749
} else if (KEYBOARD_TYPE_VISIBLE_PASSWORD.equals(keyboardType, ignoreCase = true)) {
759-
// This will supersede secureTextEntry={false}. If it doesn't, due to the way
760-
// the flags work out, the underlying field will end up a URI-type field.
761750
flagsToSet = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
762751
} else if (KEYBOARD_TYPE_URI.equals(keyboardType, ignoreCase = true)) {
763752
flagsToSet = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
764753
}
765754

766-
updateStagedInputTypeFlag(view, InputType.TYPE_MASK_CLASS, flagsToSet)
767-
checkPasswordType(view)
755+
// Deferred to onAfterUpdateTransaction(), where secureTextEntry is applied to it.
756+
view.stagedKeyboardInputType = flagsToSet
768757
}
769758

770759
@ReactProp(name = "returnKeyType")
@@ -905,6 +894,7 @@ public open class ReactTextInputManager public constructor() :
905894

906895
override fun onAfterUpdateTransaction(view: ReactEditText) {
907896
super.onAfterUpdateTransaction(view)
897+
reconcileKeyboardInputType(view)
908898
reconcileAutoCapitalize(view)
909899
view.commitStagedInputType()
910900
// setInputType() clears paint-level font variation settings, so update the typeface last to
@@ -1148,6 +1138,11 @@ public open class ReactTextInputManager public constructor() :
11481138
(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
11491139
InputType.TYPE_TEXT_FLAG_CAP_WORDS or
11501140
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS)
1141+
private const val KEYBOARD_INPUT_TYPE_MASK =
1142+
(InputType.TYPE_MASK_CLASS or
1143+
InputType.TYPE_MASK_VARIATION or
1144+
InputType.TYPE_NUMBER_FLAG_SIGNED or
1145+
InputType.TYPE_NUMBER_FLAG_DECIMAL)
11511146

11521147
private const val KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address"
11531148
private const val KEYBOARD_TYPE_NUMERIC = "numeric"
@@ -1192,21 +1187,30 @@ public open class ReactTextInputManager public constructor() :
11921187
}
11931188
}
11941189

1195-
// Sets the correct password type, since numeric and text passwords have different types
1196-
private fun checkPasswordType(view: ReactEditText) {
1197-
if (
1198-
(view.stagedInputType and INPUT_TYPE_KEYBOARD_NUMBERED) != 0 &&
1199-
(view.stagedInputType and InputType.TYPE_TEXT_VARIATION_PASSWORD) != 0
1200-
) {
1201-
// Text input type is numbered password, remove text password variation, add numeric one
1202-
updateStagedInputTypeFlag(
1203-
view,
1204-
InputType.TYPE_TEXT_VARIATION_PASSWORD,
1205-
InputType.TYPE_NUMBER_VARIATION_PASSWORD,
1206-
)
1207-
}
1190+
// A variation is a value within its class, not a set of flags: 0x10 is the URI variation of
1191+
// a text field and the password variation of a number field.
1192+
private fun reconcileKeyboardInputType(view: ReactEditText) {
1193+
val keyboardInputType =
1194+
if (view.stagedSecureTextEntry) {
1195+
toPasswordInputType(view.stagedKeyboardInputType)
1196+
} else {
1197+
view.stagedKeyboardInputType
1198+
}
1199+
view.stagedInputType =
1200+
(view.stagedInputType and KEYBOARD_INPUT_TYPE_MASK.inv()) or keyboardInputType
12081201
}
12091202

1203+
// Only the text and number classes have a password variation.
1204+
private fun toPasswordInputType(keyboardInputType: Int): Int =
1205+
when (keyboardInputType and InputType.TYPE_MASK_CLASS) {
1206+
InputType.TYPE_CLASS_TEXT ->
1207+
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
1208+
InputType.TYPE_CLASS_NUMBER ->
1209+
(keyboardInputType and InputType.TYPE_MASK_VARIATION.inv()) or
1210+
InputType.TYPE_NUMBER_VARIATION_PASSWORD
1211+
else -> keyboardInputType
1212+
}
1213+
12101214
private fun updateStagedInputTypeFlag(view: ReactEditText, flagsToUnset: Int, flagsToSet: Int) {
12111215
view.stagedInputType = (view.stagedInputType and flagsToUnset.inv()) or flagsToSet
12121216
}

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

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import android.text.Layout
2020
import android.text.SpannableString
2121
import android.text.Spanned
2222
import android.text.TextUtils
23+
import android.text.method.PasswordTransformationMethod
2324
import android.util.DisplayMetrics
2425
import android.view.Gravity
2526
import android.view.View
@@ -69,6 +70,11 @@ class ReactTextInputPropertyTest {
6970
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD and
7071
InputType.TYPE_TEXT_VARIATION_PASSWORD.inv())
7172

73+
private val classAndVariation: Int = InputType.TYPE_MASK_CLASS or InputType.TYPE_MASK_VARIATION
74+
75+
private val numberKeyboardBits: Int =
76+
classAndVariation or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED
77+
7278
@Before
7379
fun setup() {
7480
ReactNativeFeatureFlagsForTests.setUp()
@@ -406,6 +412,115 @@ class ReactTextInputPropertyTest {
406412
assertThat(view.inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero
407413
}
408414

415+
@Test
416+
fun testSecureTextEntryFalseKeepsTheKeyboardVariationInEitherPropOrder() {
417+
val textKeyboards =
418+
mapOf(
419+
"email-address" to InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
420+
"url" to InputType.TYPE_TEXT_VARIATION_URI,
421+
"visible-password" to InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
422+
)
423+
for ((keyboardType, variation) in textKeyboards) {
424+
for (secureTextEntryFirst in listOf(false, true)) {
425+
val editText = applyKeyboardProps(keyboardType, false, secureTextEntryFirst)
426+
assertThat(editText.inputType and classAndVariation)
427+
.describedAs(
428+
"%s, secureTextEntry applied first: %s",
429+
keyboardType,
430+
secureTextEntryFirst,
431+
)
432+
.isEqualTo(InputType.TYPE_CLASS_TEXT or variation)
433+
}
434+
}
435+
}
436+
437+
@Test
438+
fun testSecureTextEntryMasksEveryTextKeyboardInEitherPropOrder() {
439+
for (keyboardType in listOf("default", "email-address", "url", "visible-password")) {
440+
for (secureTextEntryFirst in listOf(false, true)) {
441+
val editText = applyKeyboardProps(keyboardType, true, secureTextEntryFirst)
442+
val subject = "$keyboardType, secureTextEntry applied first: $secureTextEntryFirst"
443+
assertThat(editText.inputType and classAndVariation)
444+
.describedAs(subject)
445+
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
446+
assertThat(editText.transformationMethod)
447+
.describedAs(subject)
448+
.isInstanceOf(PasswordTransformationMethod::class.java)
449+
}
450+
}
451+
}
452+
453+
@Test
454+
fun testSecureTextEntryUsesTheNumberPasswordForNumberKeyboards() {
455+
val numberKeyboards =
456+
mapOf(
457+
"number-pad" to InputType.TYPE_CLASS_NUMBER,
458+
"decimal-pad" to (InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL),
459+
"numeric" to
460+
(InputType.TYPE_CLASS_NUMBER or
461+
InputType.TYPE_NUMBER_FLAG_DECIMAL or
462+
InputType.TYPE_NUMBER_FLAG_SIGNED),
463+
)
464+
for ((keyboardType, keyboardInputType) in numberKeyboards) {
465+
for (secureTextEntryFirst in listOf(false, true)) {
466+
val editText = applyKeyboardProps(keyboardType, true, secureTextEntryFirst)
467+
val subject = "$keyboardType, secureTextEntry applied first: $secureTextEntryFirst"
468+
assertThat(editText.inputType and numberKeyboardBits)
469+
.describedAs(subject)
470+
.isEqualTo(keyboardInputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD)
471+
assertThat(editText.transformationMethod)
472+
.describedAs(subject)
473+
.isInstanceOf(PasswordTransformationMethod::class.java)
474+
}
475+
}
476+
}
477+
478+
@Test
479+
fun testSecureTextEntryLeavesAKeyboardWithNoPasswordVariationAsItIs() {
480+
for (secureTextEntryFirst in listOf(false, true)) {
481+
val editText = applyKeyboardProps("phone-pad", true, secureTextEntryFirst)
482+
assertThat(editText.inputType and classAndVariation)
483+
.describedAs("secureTextEntry applied first: %s", secureTextEntryFirst)
484+
.isEqualTo(InputType.TYPE_CLASS_PHONE)
485+
}
486+
}
487+
488+
@Test
489+
fun testRevealingAPasswordRestoresTheKeyboardVariation() {
490+
manager.updateProperties(view, buildStyles("keyboardType", "url", "secureTextEntry", true))
491+
manager.updateProperties(view, buildStyles("secureTextEntry", false))
492+
assertThat(view.inputType and classAndVariation)
493+
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
494+
495+
manager.updateProperties(
496+
view,
497+
buildStyles("keyboardType", "number-pad", "secureTextEntry", true),
498+
)
499+
manager.updateProperties(view, buildStyles("secureTextEntry", false))
500+
assertThat(view.inputType and numberKeyboardBits).isEqualTo(InputType.TYPE_CLASS_NUMBER)
501+
}
502+
503+
@Test
504+
fun testChangingTheKeyboardTypeReplacesItsVariationAndNumberFlags() {
505+
manager.updateProperties(view, buildStyles("keyboardType", "url"))
506+
manager.updateProperties(view, buildStyles("keyboardType", "email-address"))
507+
assertThat(view.inputType and classAndVariation)
508+
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS)
509+
510+
manager.updateProperties(view, buildStyles("keyboardType", "numeric"))
511+
manager.updateProperties(view, buildStyles("keyboardType", "number-pad"))
512+
assertThat(view.inputType and numberKeyboardBits).isEqualTo(InputType.TYPE_CLASS_NUMBER)
513+
}
514+
515+
@Test
516+
fun testSecureTextEntryStaysMaskedWithWordCapitalization() {
517+
manager.updateProperties(view, buildStyles("autoCapitalize", "words"))
518+
manager.updateProperties(view, buildStyles("secureTextEntry", true))
519+
assertThat(view.inputType and classAndVariation)
520+
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
521+
assertThat(view.transformationMethod).isInstanceOf(PasswordTransformationMethod::class.java)
522+
}
523+
409524
@Test
410525
fun testIncrementalInputTypeUpdates() {
411526
manager.updateProperties(view, buildStyles())
@@ -656,5 +771,22 @@ class ReactTextInputPropertyTest {
656771
return ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues))
657772
}
658773

774+
private fun applyKeyboardProps(
775+
keyboardType: String,
776+
secureTextEntry: Boolean,
777+
secureTextEntryFirst: Boolean,
778+
): ReactEditText {
779+
val editText = manager.createViewInstance(themedContext)
780+
if (secureTextEntryFirst) {
781+
manager.setSecureTextEntry(editText, secureTextEntry)
782+
manager.setKeyboardType(editText, keyboardType)
783+
} else {
784+
manager.setKeyboardType(editText, keyboardType)
785+
manager.setSecureTextEntry(editText, secureTextEntry)
786+
}
787+
manager.updateProperties(editText, buildStyles())
788+
return editText
789+
}
790+
659791
private class MarkerSpan
660792
}

0 commit comments

Comments
 (0)