diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField2.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField2.kt index 330f971f2b..a7f0d9224e 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField2.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField2.kt @@ -23,13 +23,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -63,18 +67,16 @@ fun TextField2( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, visualTransformation: VisualTransformation = VisualTransformation.None, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, onTextLayout: (TextLayoutResult) -> Unit = {}, modifier: Modifier = Modifier ) { - val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() BasicTextField( value = value, onValueChange = onValueChange, modifier = modifier, - textStyle = ElementTheme.typography.fontBodyLgRegular.copy( - color = if (readOnly) ElementTheme.colors.textSecondary else ElementTheme.colors.textPrimary - ), + textStyle = textFieldStyle(enabled), interactionSource = interactionSource, enabled = enabled, singleLine = singleLine, @@ -87,77 +89,202 @@ fun TextField2( visualTransformation = visualTransformation, onTextLayout = onTextLayout, ) { innerTextField -> - Column { - if (label != null) { - Text( - text = label, - color = ElementTheme.colors.textPrimary, - style = ElementTheme.typography.fontBodyMdRegular, - ) - } + DecorationBox( + label = label, + readOnly = readOnly, + enabled = enabled, + isFocused = isFocused, + isError = isError, + leadingIcon = leadingIcon, + placeholder = placeholder, + isTextEmpty = value.isEmpty(), + innerTextField = innerTextField, + trailingIcon = trailingIcon, + supportingText = supportingText + ) + } +} + +@Composable +fun TextField2( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + label: String? = null, + supportingText: String? = null, + placeholder: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + isError: Boolean = false, + enabled: Boolean = true, + readOnly: Boolean = false, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + onTextLayout: (TextLayoutResult) -> Unit = {}, + modifier: Modifier = Modifier +) { + val isFocused by interactionSource.collectIsFocusedAsState() + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier, + textStyle = textFieldStyle(enabled), + interactionSource = interactionSource, + enabled = enabled, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + readOnly = readOnly, + cursorBrush = SolidColor(ElementTheme.colors.textPrimary), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + visualTransformation = visualTransformation, + onTextLayout = onTextLayout, + ) { innerTextField -> + DecorationBox( + label = label, + readOnly = readOnly, + enabled = enabled, + isFocused = isFocused, + isError = isError, + leadingIcon = leadingIcon, + placeholder = placeholder, + isTextEmpty = value.text.isEmpty(), + innerTextField = innerTextField, + trailingIcon = trailingIcon, + supportingText = supportingText + ) + } +} + + +@Composable +private fun DecorationBox( + label: String?, + enabled: Boolean, + readOnly: Boolean, + isFocused: Boolean, + isError: Boolean, + placeholder: String?, + isTextEmpty: Boolean, + supportingText: String?, + leadingIcon: @Composable (() -> Unit)?, + trailingIcon: @Composable (() -> Unit)?, + innerTextField: @Composable () -> Unit, +) { + Column { + if (label != null) { + Text( + text = label, + color = ElementTheme.colors.textPrimary, + style = ElementTheme.typography.fontBodyMdRegular, + ) Spacer(modifier = Modifier.height(8.dp)) - Surface( - shape = RoundedCornerShape(4.dp), - border = if (readOnly) { - null - } else { - BorderStroke( - width = if (isFocused) 2.dp else 1.dp, - color = when { - isError -> ElementTheme.colors.borderCriticalPrimary - isFocused -> ElementTheme.colors.borderInteractiveHovered - else -> ElementTheme.colors.borderInteractiveSecondary - } - ) - }, - color = if (readOnly) ElementTheme.colors.bgSubtleSecondary else ElementTheme.colors.bgCanvasDefault, - ) { - Row(modifier = Modifier.padding(16.dp)) { - if (leadingIcon != null) { + } + TextFieldContainer( + enabled = enabled, + readOnly = readOnly, + isFocused = isFocused, + isError = isError + ) { + Row(modifier = Modifier.padding(16.dp)) { + if (leadingIcon != null) { + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.iconSecondary) { leadingIcon() - Spacer(modifier = Modifier.width(8.dp)) } - Box(modifier = Modifier.weight(1f)) { - if (placeholder != null && value.isEmpty()) { - Text( - text = placeholder, - color = ElementTheme.colors.textPlaceholder, - style = ElementTheme.typography.fontBodyLgRegular, - ) - } - innerTextField() + Spacer(modifier = Modifier.width(8.dp)) + } + Box(modifier = Modifier.weight(1f)) { + if (placeholder != null && isTextEmpty) { + Text( + text = placeholder, + color = ElementTheme.colors.textPlaceholder, + style = ElementTheme.typography.fontBodyLgRegular, + ) } - if (trailingIcon != null) { - Spacer(modifier = Modifier.width(8.dp)) + innerTextField() + } + if (trailingIcon != null) { + Spacer(modifier = Modifier.width(8.dp)) + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.iconSecondary) { trailingIcon() } } } - if (supportingText != null) { - Spacer(modifier = Modifier.height(4.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = spacedBy(4.dp), - ) { - if (isError) { - Icon( - imageVector = CompoundIcons.Error(), - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = ElementTheme.colors.iconCriticalPrimary - ) - } - Text( - text = supportingText, - color = if (isError) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary, - style = ElementTheme.typography.fontBodySmRegular, - ) - } - } + } + if (supportingText != null) { + Spacer(modifier = Modifier.height(4.dp)) + SupportingTextLayout(isError, supportingText) } } } +@Composable +private fun TextFieldContainer( + enabled: Boolean, + readOnly: Boolean, + isFocused: Boolean, + isError: Boolean, + content: @Composable () -> Unit +) { + Surface( + shape = RoundedCornerShape(4.dp), + border = if (readOnly) { + null + } else { + BorderStroke( + width = if (isFocused) 2.dp else 1.dp, + color = when { + !enabled -> ElementTheme.colors.borderDisabled + isError -> ElementTheme.colors.borderCriticalPrimary + isFocused -> ElementTheme.colors.borderInteractiveHovered + else -> ElementTheme.colors.borderInteractiveSecondary + } + ) + }, + color = when { + readOnly -> ElementTheme.colors.bgSubtleSecondary + !enabled -> ElementTheme.colors.bgCanvasDisabled + else -> ElementTheme.colors.bgCanvasDefault + }, + content = content + ) +} + +@Composable +private fun SupportingTextLayout(isError: Boolean, supportingText: String) { + Row(horizontalArrangement = spacedBy(4.dp)) { + if (isError) { + Icon( + imageVector = CompoundIcons.Error(), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = ElementTheme.colors.iconCriticalPrimary + ) + } + Text( + text = supportingText, + color = if (isError) ElementTheme.colors.textCriticalPrimary else ElementTheme.colors.textSecondary, + style = ElementTheme.typography.fontBodySmRegular, + ) + } +} + +@Composable +private fun textFieldStyle(enabled: Boolean): TextStyle { + return ElementTheme.typography.fontBodyLgRegular.copy( + color = if (enabled) { + ElementTheme.colors.textPrimary + } else { + ElementTheme.colors.textSecondary + } + ) +} + @Preview(group = PreviewGroup.TextFields) @Composable internal fun TextFields2LightPreview() = ElementPreviewLight { ContentToPreview() }