From 612dfa4c9a868d4488058d557f0334fa40e7f889 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 7 Nov 2024 18:34:44 +0100 Subject: [PATCH] design : new TextField api --- .../impl/configureroom/ConfigureRoomView.kt | 78 +++---- .../theme/components/TextField2.kt | 190 ++++++++++++++++++ 2 files changed, 221 insertions(+), 47 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField2.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index b0084f68f6..ed490790dc 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -45,13 +44,14 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.modifiers.clearFocusOnTap import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewWithLargeHeight import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.TextField2 import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton -import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.components.AvatarActionBottomSheet @@ -90,10 +90,10 @@ fun ConfigureRoomView( ) { padding -> Column( modifier = Modifier - .padding(padding) - .imePadding() - .verticalScroll(rememberScrollState()) - .consumeWindowInsets(padding), + .padding(padding) + .imePadding() + .verticalScroll(rememberScrollState()) + .consumeWindowInsets(padding), verticalArrangement = Arrangement.spacedBy(24.dp), ) { RoomNameWithAvatar( @@ -217,7 +217,7 @@ private fun RoomNameWithAvatar( modifier = Modifier.clickable(onClick = onAvatarClick), ) - LabelledTextField( + TextField2( label = stringResource(R.string.screen_create_room_room_name_label), value = roomName, placeholder = stringResource(CommonStrings.common_room_name_placeholder), @@ -233,7 +233,7 @@ private fun RoomTopic( onTopicChange: (String) -> Unit, modifier: Modifier = Modifier, ) { - LabelledTextField( + TextField2( modifier = modifier, label = stringResource(R.string.screen_create_room_topic_label), value = topic, @@ -322,49 +322,33 @@ private fun RoomAddressField( onAddressChange: (String) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - style = ElementTheme.typography.fontBodyMdRegular, - color = MaterialTheme.colorScheme.primary, - text = stringResource(R.string.screen_create_room_room_address_section_title), - ) - TextField( - modifier = Modifier.fillMaxWidth(), - value = address.value, - leadingIcon = { - Text( - text = "#", - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textSecondary, - ) - }, - trailingIcon = { - Text( - text = homeserverName, - style = ElementTheme.typography.fontBodyLgMedium, - color = ElementTheme.colors.textSecondary, - modifier = Modifier.padding(end = 16.dp) - ) - }, - supportingText = { - Text( - text = stringResource(R.string.screen_create_room_room_address_section_footer), - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, - ) - }, - onValueChange = onAddressChange, - singleLine = true, - ) - } + TextField2( + modifier = modifier.fillMaxWidth(), + value = address.value, + label = stringResource(R.string.screen_create_room_room_address_section_title), + leadingIcon = { + Text( + text = "#", + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textSecondary, + ) + }, + trailingIcon = { + Text( + text = homeserverName, + style = ElementTheme.typography.fontBodyLgMedium, + color = ElementTheme.colors.textSecondary, + ) + }, + supportingText = stringResource(R.string.screen_create_room_room_address_section_footer), + onValueChange = onAddressChange, + singleLine = true, + ) } @PreviewsDayNight +@PreviewWithLargeHeight @Composable internal fun ConfigureRoomViewPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = ElementPreview { ConfigureRoomView( 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 new file mode 100644 index 0000000000..330f971f2b --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField2.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2023, 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.runtime.Composable +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.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.PreviewGroup +import io.element.android.libraries.designsystem.utils.allBooleans +import io.element.android.libraries.designsystem.utils.asInt + +/** + * https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2008-37137 + */ +@Composable +fun TextField2( + value: String, + onValueChange: (String) -> 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, + 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 + ), + 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 -> + 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) { + 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() + } + if (trailingIcon != null) { + Spacer(modifier = Modifier.width(8.dp)) + 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, + ) + } + } + } + } +} + +@Preview(group = PreviewGroup.TextFields) +@Composable +internal fun TextFields2LightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview(group = PreviewGroup.TextFields) +@Composable +internal fun TextFields2DarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +@ExcludeFromCoverage +private fun ContentToPreview() { + Column(modifier = Modifier.padding(4.dp)) { + allBooleans.forEach { isError -> + allBooleans.forEach { enabled -> + allBooleans.forEach { readonly -> + TextField2( + onValueChange = {}, + label = "Label", + value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}", + supportingText = "Supporting text", + isError = isError, + enabled = enabled, + readOnly = readonly, + ) + Spacer(modifier = Modifier.height(2.dp)) + } + } + } + } +}