Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ public open class ReactEditText public constructor(context: Context) : AppCompat

public var stagedInputType: Int
internal var stagedAutoCapitalize: Int = 0
internal var stagedKeyboardInputType: Int = InputType.TYPE_CLASS_TEXT
internal var stagedSecureTextEntry: Boolean = false
public var submitBehavior: String? = null
public var dragAndDropFilter: List<String>? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,17 +696,8 @@ public open class ReactTextInputManager public constructor() :

@ReactProp(name = "secureTextEntry", defaultBoolean = false)
public fun setSecureTextEntry(view: ReactEditText, password: Boolean) {
updateStagedInputTypeFlag(
view = view,
flagsToUnset =
if (password) {
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
} else {
InputType.TYPE_NUMBER_VARIATION_PASSWORD or InputType.TYPE_TEXT_VARIATION_PASSWORD
},
flagsToSet = if (password) InputType.TYPE_TEXT_VARIATION_PASSWORD else 0,
)
checkPasswordType(view)
// Deferred to onAfterUpdateTransaction(), where it is applied to the keyboard type.
view.stagedSecureTextEntry = password
}

// This prop temporarily takes both numbers and strings.
Expand Down Expand Up @@ -756,15 +747,13 @@ public open class ReactTextInputManager public constructor() :
} 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)) {
// This will supersede secureTextEntry={false}. If it doesn't, due to the way
// the flags work out, the underlying field will end up a URI-type field.
flagsToSet = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
} else if (KEYBOARD_TYPE_URI.equals(keyboardType, ignoreCase = true)) {
flagsToSet = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI
}

updateStagedInputTypeFlag(view, InputType.TYPE_MASK_CLASS, flagsToSet)
checkPasswordType(view)
// Deferred to onAfterUpdateTransaction(), where secureTextEntry is applied to it.
view.stagedKeyboardInputType = flagsToSet
}

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

override fun onAfterUpdateTransaction(view: ReactEditText) {
super.onAfterUpdateTransaction(view)
reconcileKeyboardInputType(view)
reconcileAutoCapitalize(view)
view.commitStagedInputType()
// setInputType() clears paint-level font variation settings, so update the typeface last to
Expand Down Expand Up @@ -1148,6 +1138,11 @@ public open class ReactTextInputManager public constructor() :
(InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or
InputType.TYPE_TEXT_FLAG_CAP_WORDS or
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS)
private const val KEYBOARD_INPUT_TYPE_MASK =
(InputType.TYPE_MASK_CLASS or
InputType.TYPE_MASK_VARIATION or
InputType.TYPE_NUMBER_FLAG_SIGNED or
InputType.TYPE_NUMBER_FLAG_DECIMAL)

private const val KEYBOARD_TYPE_EMAIL_ADDRESS = "email-address"
private const val KEYBOARD_TYPE_NUMERIC = "numeric"
Expand Down Expand Up @@ -1192,21 +1187,30 @@ public open class ReactTextInputManager public constructor() :
}
}

// Sets the correct password type, since numeric and text passwords have different types
private fun checkPasswordType(view: ReactEditText) {
if (
(view.stagedInputType and INPUT_TYPE_KEYBOARD_NUMBERED) != 0 &&
(view.stagedInputType and InputType.TYPE_TEXT_VARIATION_PASSWORD) != 0
) {
// Text input type is numbered password, remove text password variation, add numeric one
updateStagedInputTypeFlag(
view,
InputType.TYPE_TEXT_VARIATION_PASSWORD,
InputType.TYPE_NUMBER_VARIATION_PASSWORD,
)
}
// A variation is a value within its class, not a set of flags: 0x10 is the URI variation of
// a text field and the password variation of a number field.
private fun reconcileKeyboardInputType(view: ReactEditText) {
val keyboardInputType =
if (view.stagedSecureTextEntry) {
toPasswordInputType(view.stagedKeyboardInputType)
} else {
view.stagedKeyboardInputType
}
view.stagedInputType =
(view.stagedInputType and KEYBOARD_INPUT_TYPE_MASK.inv()) or keyboardInputType
}

// Only the text and number classes have a password variation.
private fun toPasswordInputType(keyboardInputType: Int): Int =
when (keyboardInputType and InputType.TYPE_MASK_CLASS) {
InputType.TYPE_CLASS_TEXT ->
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
InputType.TYPE_CLASS_NUMBER ->
(keyboardInputType and InputType.TYPE_MASK_VARIATION.inv()) or
InputType.TYPE_NUMBER_VARIATION_PASSWORD
else -> keyboardInputType
}

private fun updateStagedInputTypeFlag(view: ReactEditText, flagsToUnset: Int, flagsToSet: Int) {
view.stagedInputType = (view.stagedInputType and flagsToUnset.inv()) or flagsToSet
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.text.Layout
import android.text.SpannableString
import android.text.Spanned
import android.text.TextUtils
import android.text.method.PasswordTransformationMethod
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.View
Expand Down Expand Up @@ -69,6 +70,11 @@ class ReactTextInputPropertyTest {
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD and
InputType.TYPE_TEXT_VARIATION_PASSWORD.inv())

private val classAndVariation: Int = InputType.TYPE_MASK_CLASS or InputType.TYPE_MASK_VARIATION

private val numberKeyboardBits: Int =
classAndVariation or InputType.TYPE_NUMBER_FLAG_DECIMAL or InputType.TYPE_NUMBER_FLAG_SIGNED

@Before
fun setup() {
ReactNativeFeatureFlagsForTests.setUp()
Expand Down Expand Up @@ -406,6 +412,115 @@ class ReactTextInputPropertyTest {
assertThat(view.inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD).isZero
}

@Test
fun testSecureTextEntryFalseKeepsTheKeyboardVariationInEitherPropOrder() {
val textKeyboards =
mapOf(
"email-address" to InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
"url" to InputType.TYPE_TEXT_VARIATION_URI,
"visible-password" to InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD,
)
for ((keyboardType, variation) in textKeyboards) {
for (secureTextEntryFirst in listOf(false, true)) {
val editText = applyKeyboardProps(keyboardType, false, secureTextEntryFirst)
assertThat(editText.inputType and classAndVariation)
.describedAs(
"%s, secureTextEntry applied first: %s",
keyboardType,
secureTextEntryFirst,
)
.isEqualTo(InputType.TYPE_CLASS_TEXT or variation)
}
}
}

@Test
fun testSecureTextEntryMasksEveryTextKeyboardInEitherPropOrder() {
for (keyboardType in listOf("default", "email-address", "url", "visible-password")) {
for (secureTextEntryFirst in listOf(false, true)) {
val editText = applyKeyboardProps(keyboardType, true, secureTextEntryFirst)
val subject = "$keyboardType, secureTextEntry applied first: $secureTextEntryFirst"
assertThat(editText.inputType and classAndVariation)
.describedAs(subject)
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
assertThat(editText.transformationMethod)
.describedAs(subject)
.isInstanceOf(PasswordTransformationMethod::class.java)
}
}
}

@Test
fun testSecureTextEntryUsesTheNumberPasswordForNumberKeyboards() {
val numberKeyboards =
mapOf(
"number-pad" to InputType.TYPE_CLASS_NUMBER,
"decimal-pad" to (InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL),
"numeric" to
(InputType.TYPE_CLASS_NUMBER or
InputType.TYPE_NUMBER_FLAG_DECIMAL or
InputType.TYPE_NUMBER_FLAG_SIGNED),
)
for ((keyboardType, keyboardInputType) in numberKeyboards) {
for (secureTextEntryFirst in listOf(false, true)) {
val editText = applyKeyboardProps(keyboardType, true, secureTextEntryFirst)
val subject = "$keyboardType, secureTextEntry applied first: $secureTextEntryFirst"
assertThat(editText.inputType and numberKeyboardBits)
.describedAs(subject)
.isEqualTo(keyboardInputType or InputType.TYPE_NUMBER_VARIATION_PASSWORD)
assertThat(editText.transformationMethod)
.describedAs(subject)
.isInstanceOf(PasswordTransformationMethod::class.java)
}
}
}

@Test
fun testSecureTextEntryLeavesAKeyboardWithNoPasswordVariationAsItIs() {
for (secureTextEntryFirst in listOf(false, true)) {
val editText = applyKeyboardProps("phone-pad", true, secureTextEntryFirst)
assertThat(editText.inputType and classAndVariation)
.describedAs("secureTextEntry applied first: %s", secureTextEntryFirst)
.isEqualTo(InputType.TYPE_CLASS_PHONE)
}
}

@Test
fun testRevealingAPasswordRestoresTheKeyboardVariation() {
manager.updateProperties(view, buildStyles("keyboardType", "url", "secureTextEntry", true))
manager.updateProperties(view, buildStyles("secureTextEntry", false))
assertThat(view.inputType and classAndVariation)
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)

manager.updateProperties(
view,
buildStyles("keyboardType", "number-pad", "secureTextEntry", true),
)
manager.updateProperties(view, buildStyles("secureTextEntry", false))
assertThat(view.inputType and numberKeyboardBits).isEqualTo(InputType.TYPE_CLASS_NUMBER)
}

@Test
fun testChangingTheKeyboardTypeReplacesItsVariationAndNumberFlags() {
manager.updateProperties(view, buildStyles("keyboardType", "url"))
manager.updateProperties(view, buildStyles("keyboardType", "email-address"))
assertThat(view.inputType and classAndVariation)
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS)

manager.updateProperties(view, buildStyles("keyboardType", "numeric"))
manager.updateProperties(view, buildStyles("keyboardType", "number-pad"))
assertThat(view.inputType and numberKeyboardBits).isEqualTo(InputType.TYPE_CLASS_NUMBER)
}

@Test
fun testSecureTextEntryStaysMaskedWithWordCapitalization() {
manager.updateProperties(view, buildStyles("autoCapitalize", "words"))
manager.updateProperties(view, buildStyles("secureTextEntry", true))
assertThat(view.inputType and classAndVariation)
.isEqualTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD)
assertThat(view.transformationMethod).isInstanceOf(PasswordTransformationMethod::class.java)
}

@Test
fun testIncrementalInputTypeUpdates() {
manager.updateProperties(view, buildStyles())
Expand Down Expand Up @@ -656,5 +771,22 @@ class ReactTextInputPropertyTest {
return ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues))
}

private fun applyKeyboardProps(
keyboardType: String,
secureTextEntry: Boolean,
secureTextEntryFirst: Boolean,
): ReactEditText {
val editText = manager.createViewInstance(themedContext)
if (secureTextEntryFirst) {
manager.setSecureTextEntry(editText, secureTextEntry)
manager.setKeyboardType(editText, keyboardType)
} else {
manager.setKeyboardType(editText, keyboardType)
manager.setSecureTextEntry(editText, secureTextEntry)
}
manager.updateProperties(editText, buildStyles())
return editText
}

private class MarkerSpan
}
Loading