mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 16:10:00 +00:00
Merge pull request #7132 from element-hq/fix/a11y-pin-screen
Add programmatic label and focus indicator to PIN entry field
This commit is contained in:
+49
-18
@@ -15,15 +15,23 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
@@ -32,6 +40,8 @@ import io.element.android.features.lockscreen.impl.pin.model.PinEntry
|
|||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.pinDigitBg
|
import io.element.android.libraries.designsystem.theme.pinDigitBg
|
||||||
|
import io.element.android.libraries.ui.strings.CommonPlurals
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PinEntryTextField(
|
fun PinEntryTextField(
|
||||||
@@ -39,16 +49,25 @@ fun PinEntryTextField(
|
|||||||
isSecured: Boolean,
|
isSecured: Boolean,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
initialIsFocus: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
var isFocused by remember { mutableStateOf(initialIsFocus) }
|
||||||
|
val filledCount = pinEntry.digits.count { it is PinDigit.Filled }
|
||||||
|
val pinFieldLabel = stringResource(CommonStrings.a11y_pin_field)
|
||||||
|
val digitsEnteredLabel = pluralStringResource(CommonPlurals.a11y_digits_entered, filledCount, filledCount)
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
modifier = modifier,
|
modifier = modifier
|
||||||
|
.onFocusChanged { isFocused = it.isFocused }
|
||||||
|
.clearAndSetSemantics {
|
||||||
|
contentDescription = "$pinFieldLabel, $digitsEnteredLabel"
|
||||||
|
},
|
||||||
value = pinEntry.toText(),
|
value = pinEntry.toText(),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
onValueChange(it)
|
onValueChange(it)
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword),
|
||||||
decorationBox = {
|
decorationBox = {
|
||||||
PinEntryRow(pinEntry = pinEntry, isSecured = isSecured)
|
PinEntryRow(pinEntry = pinEntry, isSecured = isSecured, isFocused = isFocused)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -58,13 +77,19 @@ fun PinEntryTextField(
|
|||||||
private fun PinEntryRow(
|
private fun PinEntryRow(
|
||||||
pinEntry: PinEntry,
|
pinEntry: PinEntry,
|
||||||
isSecured: Boolean,
|
isSecured: Boolean,
|
||||||
|
isFocused: Boolean,
|
||||||
) {
|
) {
|
||||||
FlowRow(
|
FlowRow(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.CenterHorizontally),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
for (digit in pinEntry.digits) {
|
val focusedIndex = pinEntry.digits.indexOfFirst { it is PinDigit.Empty }
|
||||||
PinDigitView(digit = digit, isSecured = isSecured)
|
pinEntry.digits.forEachIndexed { index, digit ->
|
||||||
|
PinDigitView(
|
||||||
|
digit = digit,
|
||||||
|
isSecured = isSecured,
|
||||||
|
isFocused = isFocused && index == focusedIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,11 +98,16 @@ private fun PinEntryRow(
|
|||||||
private fun PinDigitView(
|
private fun PinDigitView(
|
||||||
digit: PinDigit,
|
digit: PinDigit,
|
||||||
isSecured: Boolean,
|
isSecured: Boolean,
|
||||||
|
isFocused: Boolean,
|
||||||
) {
|
) {
|
||||||
val shape = RoundedCornerShape(8.dp)
|
val shape = RoundedCornerShape(8.dp)
|
||||||
val appearanceModifier = when (digit) {
|
val appearanceModifier = when (digit) {
|
||||||
PinDigit.Empty -> {
|
PinDigit.Empty -> {
|
||||||
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
|
if (isFocused) {
|
||||||
|
Modifier.border(2.dp, ElementTheme.colors.borderFocused, shape)
|
||||||
|
} else {
|
||||||
|
Modifier.border(1.dp, ElementTheme.colors.iconPrimary, shape)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is PinDigit.Filled -> {
|
is PinDigit.Filled -> {
|
||||||
Modifier.background(ElementTheme.colors.pinDigitBg, shape)
|
Modifier.background(ElementTheme.colors.pinDigitBg, shape)
|
||||||
@@ -108,18 +138,19 @@ private fun PinDigitView(
|
|||||||
internal fun PinEntryTextFieldPreview() {
|
internal fun PinEntryTextFieldPreview() {
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
val pinEntry = PinEntry.createEmpty(4).fillWith("12")
|
val pinEntry = PinEntry.createEmpty(4).fillWith("12")
|
||||||
Column {
|
Column(
|
||||||
PinEntryTextField(
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
pinEntry = pinEntry,
|
) {
|
||||||
isSecured = true,
|
listOf(true, false).forEach { isSecured ->
|
||||||
onValueChange = {},
|
listOf(true, false).forEach { initialIsFocus ->
|
||||||
)
|
PinEntryTextField(
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
pinEntry = pinEntry,
|
||||||
PinEntryTextField(
|
isSecured = isSecured,
|
||||||
pinEntry = pinEntry,
|
initialIsFocus = initialIsFocus,
|
||||||
isSecured = false,
|
onValueChange = {},
|
||||||
onValueChange = {},
|
)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user