Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add formatted content description for Expiration Date text field #10185

Merged
merged 21 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions stripe-ui-core/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,12 @@
<string name="stripe_invalid_expiry_month">Your card\'s expiration month is invalid.</string>
<!-- String to describe an invalid year in expiry date. -->
<string name="stripe_invalid_expiry_year">Your card\'s expiration year is invalid.</string>
<!-- String to describe an empty expiration date text field. -->
<string name="stripe_expiration_date_empty_content_description">Expiration date. Two digit month and two digit year, empty</string>
<!-- String to describe an expiration date text field with complete month and empty year. -->
<string name="stripe_expiration_date_month_complete_content_description">Expiration date. %s and two digit year, empty</string>
<!-- String to describe an expiration date text field with complete month and incomplete year. -->
<string name="stripe_expiration_date_year_incomplete_content_description">Expiration date. %s and two digit year, incomplete</string>
<!-- String to describe an complete expiration date text field. -->
<string name="stripe_expiration_date_content_description">Expiration date. %s %s</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,21 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.editableText
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import com.stripe.android.core.Logger
import com.stripe.android.uicore.BuildConfig
Expand All @@ -67,6 +72,7 @@ import com.stripe.android.uicore.moveFocusSafely
import com.stripe.android.uicore.stripeColors
import com.stripe.android.uicore.text.autofill
import com.stripe.android.uicore.utils.collectAsState
import com.stripe.android.uicore.utils.formatExpirationDateForAccessibility
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -123,6 +129,7 @@ fun TextFieldSection(
* attribute of [textFieldController] is also taken into account to decide if the UI should be
* enabled.
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@Suppress("LongMethod")
fun TextField(
Expand Down Expand Up @@ -166,6 +173,10 @@ fun TextField(
mutableStateOf<TextRange?>(null)
}

val context = LocalContext.current

val isExpiryDate = (textFieldController as? SimpleTextFieldController)?.textFieldConfig is DateConfig

TextFieldUi(
value = TextFieldValue(
text = value,
Expand Down Expand Up @@ -207,7 +218,13 @@ fun TextField(
)
.focusRequester(focusRequester)
.semantics {
this.contentDescription = contentDescription
if (isExpiryDate) {
this.editableText = AnnotatedString("")
this.contentDescription = formatExpirationDateForAccessibility(Locale.current, value)
.resolve(context)
} else {
this.contentDescription = contentDescription
}
},
enabled = enabled && textFieldController.enabled,
label = label?.let {
Expand Down Expand Up @@ -268,6 +285,11 @@ internal fun TextFieldUi(
)
} else {
it
},
modifier = if (it == stringResource(R.string.stripe_expiration_date_hint)){
Modifier.clearAndSetSemantics {}
} else {
Modifier
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.stripe.android.uicore.utils

import androidx.compose.ui.text.intl.Locale as ComposeLocale
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.uicore.R
import java.text.SimpleDateFormat
import java.util.Locale as JavaLocale

internal fun formatExpirationDateForAccessibility(locale: ComposeLocale, input: String): ResolvableString {
if (input.isEmpty()) {
return resolvableString(R.string.stripe_expiration_date_empty_content_description)
}

val canOnlyBeSingleDigitMonth = input.isNotBlank() && !(input[0] == '0' || input[0] == '1')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This split logic taken from ExpiryDateVisualTransformation

val canOnlyBeJanuary = input.length > 1 && input.take(2).toInt() > 12
val isSingleDigitMonth = canOnlyBeSingleDigitMonth || canOnlyBeJanuary

val lastIndexOfMonth = if (isSingleDigitMonth) 0 else 1
val month = input.take(lastIndexOfMonth + 1).toIntOrNull()
val year = input.slice(lastIndexOfMonth + 1..input.lastIndex).toIntOrNull()

try {
val javaLocale= JavaLocale(locale.language, locale.region)
if (month != null && year == null) {
if (month in 1..12) {
val monthName =
SimpleDateFormat("MM", javaLocale).parse("$month")?.let {
SimpleDateFormat("MMMM", javaLocale).format(it)
}

return resolvableString(
R.string.stripe_expiration_date_month_complete_content_description,
monthName
)
}
}

if (month != null && year != null) {
if (month in 1..12) {
val monthName =
SimpleDateFormat("MM", javaLocale).parse("$month")?.let {
SimpleDateFormat("MMMM", javaLocale).format(it)
}
return if (year <= 9) {
resolvableString(
R.string.stripe_expiration_date_year_incomplete_content_description,
monthName
)
} else {
resolvableString(
R.string.stripe_expiration_date_content_description,
monthName,
2000 + year
)
}
}
}

// If we can't parse it, return the original input
return input.resolvableString
} catch (e: Exception) {
return input.resolvableString
}
}
Loading