design(text field) : allow setting validity (instead of just isError bool)

This commit is contained in:
ganfra 2025-02-20 21:49:50 +01:00
parent f1c5fa53e8
commit f6b4ee945b
6 changed files with 59 additions and 31 deletions

View file

@ -29,6 +29,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -98,7 +99,11 @@ private fun RoomAddressField(
"e.g. #room-name:matrix.org" "e.g. #room-name:matrix.org"
} }
}, },
isError = addressState is RoomAddressState.Invalid, validity = when (addressState) {
RoomAddressState.Unknown -> null
RoomAddressState.Invalid -> TextFieldValidity.Invalid
is RoomAddressState.Valid -> if (addressState.matchingRoomFound) TextFieldValidity.Valid else null
},
onValueChange = onAddressChange, onValueChange = onAddressChange,
singleLine = true, singleLine = true,
) )

View file

@ -45,6 +45,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -90,7 +91,7 @@ fun BugReportView(
keyboardController?.hide() keyboardController?.hide()
}), }),
minLines = 3, minLines = 3,
isError = state.isDescriptionInError, validity = if(state.isDescriptionInError) TextFieldValidity.Invalid else null,
) )
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View file

@ -33,6 +33,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
@ -82,8 +83,8 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool
var showPassword by remember { mutableStateOf(false) } var showPassword by remember { mutableStateOf(false) }
TextField( TextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.onTabOrEnterKeyFocusNext(LocalFocusManager.current), .onTabOrEnterKeyFocusNext(LocalFocusManager.current),
value = text, value = text,
onValueChange = onTextChange, onValueChange = onTextChange,
placeholder = stringResource(CommonStrings.common_password), placeholder = stringResource(CommonStrings.common_password),
@ -99,7 +100,7 @@ private fun Content(text: String, onTextChange: (String) -> Unit, hasError: Bool
Icon(imageVector = image, description) Icon(imageVector = image, description)
} }
}, },
isError = hasError, validity = if (hasError) TextFieldValidity.Invalid else null,
supportingText = if (hasError) { supportingText = if (hasError) {
stringResource(R.string.screen_reset_encryption_password_error) stringResource(R.string.screen_reset_encryption_password_error)
} else { } else {

View file

@ -58,7 +58,7 @@ fun TextField(
placeholder: String? = null, placeholder: String? = null,
leadingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false, validity: TextFieldValidity? = null,
enabled: Boolean = true, enabled: Boolean = true,
readOnly: Boolean = false, readOnly: Boolean = false,
singleLine: Boolean = false, singleLine: Boolean = false,
@ -93,7 +93,7 @@ fun TextField(
readOnly = readOnly, readOnly = readOnly,
enabled = enabled, enabled = enabled,
isFocused = isFocused, isFocused = isFocused,
isError = isError, validity = validity,
leadingIcon = leadingIcon, leadingIcon = leadingIcon,
placeholder = placeholder, placeholder = placeholder,
isTextEmpty = value.isEmpty(), isTextEmpty = value.isEmpty(),
@ -114,7 +114,7 @@ fun TextField(
placeholder: String? = null, placeholder: String? = null,
leadingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false, validity: TextFieldValidity? = null,
enabled: Boolean = true, enabled: Boolean = true,
readOnly: Boolean = false, readOnly: Boolean = false,
singleLine: Boolean = false, singleLine: Boolean = false,
@ -149,7 +149,7 @@ fun TextField(
readOnly = readOnly, readOnly = readOnly,
enabled = enabled, enabled = enabled,
isFocused = isFocused, isFocused = isFocused,
isError = isError, validity = validity,
leadingIcon = leadingIcon, leadingIcon = leadingIcon,
placeholder = placeholder, placeholder = placeholder,
isTextEmpty = value.text.isEmpty(), isTextEmpty = value.text.isEmpty(),
@ -166,7 +166,7 @@ private fun DecorationBox(
enabled: Boolean, enabled: Boolean,
readOnly: Boolean, readOnly: Boolean,
isFocused: Boolean, isFocused: Boolean,
isError: Boolean, validity: TextFieldValidity?,
placeholder: String?, placeholder: String?,
isTextEmpty: Boolean, isTextEmpty: Boolean,
supportingText: String?, supportingText: String?,
@ -187,7 +187,7 @@ private fun DecorationBox(
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
isFocused = isFocused, isFocused = isFocused,
isError = isError isError = validity == TextFieldValidity.Invalid
) { ) {
Row(modifier = Modifier.padding(16.dp)) { Row(modifier = Modifier.padding(16.dp)) {
if (leadingIcon != null) { if (leadingIcon != null) {
@ -216,7 +216,7 @@ private fun DecorationBox(
} }
if (supportingText != null) { if (supportingText != null) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
SupportingTextLayout(isError, supportingText) SupportingTextLayout(validity, supportingText)
} }
} }
} }
@ -254,24 +254,44 @@ private fun TextFieldContainer(
} }
@Composable @Composable
private fun SupportingTextLayout(isError: Boolean, supportingText: String) { private fun SupportingTextLayout(validity: TextFieldValidity?, supportingText: String) {
Row(horizontalArrangement = spacedBy(4.dp)) { Row(horizontalArrangement = spacedBy(4.dp)) {
if (isError) { when (validity) {
Icon( TextFieldValidity.Invalid -> {
imageVector = CompoundIcons.Error(), Icon(
contentDescription = null, imageVector = CompoundIcons.Error(),
modifier = Modifier.size(16.dp), contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary modifier = Modifier.size(16.dp),
) tint = ElementTheme.colors.iconCriticalPrimary
)
}
TextFieldValidity.Valid -> {
Icon(
imageVector = CompoundIcons.CheckCircleSolid(),
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = ElementTheme.colors.iconSuccessPrimary
)
}
else -> Unit
} }
Text( Text(
text = supportingText, text = supportingText,
color = if (isError) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary, color = when (validity) {
TextFieldValidity.Invalid -> ElementTheme.colors.textCriticalPrimary
TextFieldValidity.Valid -> ElementTheme.colors.textSuccessPrimary
else -> ElementTheme.colors.textSecondary
},
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
) )
} }
} }
enum class TextFieldValidity {
Invalid,
Valid
}
@Composable @Composable
private fun textFieldStyle(enabled: Boolean): TextStyle { private fun textFieldStyle(enabled: Boolean): TextStyle {
return ElementTheme.typography.fontBodyLgRegular.copy( return ElementTheme.typography.fontBodyLgRegular.copy(
@ -283,11 +303,11 @@ private fun textFieldStyle(enabled: Boolean): TextStyle {
) )
} }
@Preview(group = PreviewGroup.TextFields) @Preview(group = PreviewGroup.TextFields, heightDp = 1000)
@Composable @Composable
internal fun TextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() } internal fun TextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview(group = PreviewGroup.TextFields) @Preview(group = PreviewGroup.TextFields, heightDp = 1000)
@Composable @Composable
internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() } internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
@ -295,15 +315,15 @@ internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
@ExcludeFromCoverage @ExcludeFromCoverage
private fun ContentToPreview() { private fun ContentToPreview() {
Column(modifier = Modifier.padding(4.dp)) { Column(modifier = Modifier.padding(4.dp)) {
allBooleans.forEach { isError -> TextFieldValidity.entries.forEach { validity ->
allBooleans.forEach { enabled -> allBooleans.forEach { enabled ->
allBooleans.forEach { readonly -> allBooleans.forEach { readonly ->
TextField( TextField(
onValueChange = {}, onValueChange = {},
label = "Label", label = "Label",
value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}", value = "Hello val=${validity}, en=${enabled.asInt()}, ro=${readonly.asInt()}",
supportingText = "Supporting text", supportingText = "Supporting text",
isError = isError, validity = validity,
enabled = enabled, enabled = enabled,
readOnly = readonly, readOnly = readonly,
) )

View file

@ -15,6 +15,7 @@ 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.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TextField
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@ -56,7 +57,10 @@ fun RoomAddressField(
} }
else -> supportingText else -> supportingText
}, },
isError = addressValidity.isError(), validity = when (addressValidity) {
RoomAddressValidity.InvalidSymbols, RoomAddressValidity.NotAvailable -> TextFieldValidity.Invalid
else -> null
},
onValueChange = onAddressChange, onValueChange = onAddressChange,
singleLine = true, singleLine = true,
) )

View file

@ -8,6 +8,7 @@
package io.element.android.libraries.matrix.ui.room.address package io.element.android.libraries.matrix.ui.room.address
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.theme.components.TextFieldValidity
/** /**
* Represents the validity state of a room address. * Represents the validity state of a room address.
@ -19,8 +20,4 @@ sealed interface RoomAddressValidity {
data object InvalidSymbols : RoomAddressValidity data object InvalidSymbols : RoomAddressValidity
data object NotAvailable : RoomAddressValidity data object NotAvailable : RoomAddressValidity
data object Valid : RoomAddressValidity data object Valid : RoomAddressValidity
fun isError(): Boolean {
return this is InvalidSymbols || this is NotAvailable
}
} }