Merge pull request #3834 from element-hq/feature/fga/design_system_text_field

Design system : implement new TextField
This commit is contained in:
ganfra 2024-11-12 11:18:07 +01:00 committed by GitHub
commit cffd2da10b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
188 changed files with 817 additions and 901 deletions

View file

@ -1,76 +0,0 @@
/*
* 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.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun LabelledOutlinedTextField(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: String? = null,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.primary,
text = label
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = value,
placeholder = placeholder?.let { { Text(placeholder) } },
onValueChange = onValueChange,
singleLine = singleLine,
maxLines = maxLines,
keyboardOptions = keyboardOptions,
)
}
}
@PreviewsDayNight
@Composable
internal fun LabelledOutlinedTextFieldPreview() = ElementPreview {
Column {
LabelledOutlinedTextField(
label = "Room name",
value = "",
onValueChange = {},
placeholder = "e.g. Product Sprint",
)
LabelledOutlinedTextField(
label = "Room name",
value = "a room name",
onValueChange = {},
placeholder = "e.g. Product Sprint",
)
}
}

View file

@ -1,76 +0,0 @@
/*
* 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.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementPreview
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.TextField
@Composable
fun LabelledTextField(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: String? = null,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.primary,
text = label
)
TextField(
modifier = Modifier.fillMaxWidth(),
value = value,
placeholder = placeholder?.let { { Text(placeholder) } },
onValueChange = onValueChange,
singleLine = singleLine,
maxLines = maxLines,
keyboardOptions = keyboardOptions,
)
}
}
@PreviewsDayNight
@Composable
internal fun LabelledTextFieldPreview() = ElementPreview {
Column {
LabelledTextField(
label = "Room name",
value = "",
onValueChange = {},
placeholder = "e.g. Product Sprint",
)
LabelledTextField(
label = "Room name",
value = "a room name",
onValueChange = {},
placeholder = "e.g. Product Sprint",
)
}
}

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.designsystem.components.list
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -18,7 +19,6 @@ import androidx.compose.ui.tooling.preview.Preview
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.OutlinedTextField
import io.element.android.libraries.designsystem.theme.components.Text
@Composable

View file

@ -0,0 +1,43 @@
/*
* Copyright 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.modifiers
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillNode
import androidx.compose.ui.autofill.AutofillType
import androidx.compose.ui.composed
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
@Suppress("ModifierComposed")
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.autofill(autofillTypes: List<AutofillType>, onFill: (String) -> Unit) = composed {
val autofillNode = AutofillNode(autofillTypes, onFill = onFill)
LocalAutofillTree.current += autofillNode
val autofill = LocalAutofill.current
this
.onGloballyPositioned {
// Inform autofill framework of where our composable is so it can show the popup in the right place
autofillNode.boundingBox = it.boundsInWindow()
}
.onFocusChanged {
autofill?.run {
if (it.isFocused) {
requestAutofillForNode(autofillNode)
} else {
cancelAutofillForNode(autofillNode)
}
}
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 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.modifiers
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
fun Modifier.onTabOrEnterKeyFocusNext(focusManager: FocusManager): Modifier = onPreviewKeyEvent { event ->
if (event.key == Key.Tab || event.key == Key.Enter) {
if (event.type == KeyEventType.KeyUp) {
focusManager.moveFocus(FocusDirection.Down)
}
true
} else {
false
}
}

View file

@ -15,25 +15,19 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.text.TextRange
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
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@ -42,7 +36,7 @@ import io.element.android.libraries.designsystem.utils.allBooleans
import io.element.android.libraries.designsystem.utils.asInt
@Composable
fun OutlinedTextField(
fun FilledTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
@ -60,12 +54,16 @@ fun OutlinedTextField(
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = OutlinedTextFieldDefaults.shape,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors(
unfocusedContainerColor = ElementTheme.colors.bgSubtleSecondary,
focusedContainerColor = ElementTheme.colors.bgSubtleSecondary,
disabledContainerColor = ElementTheme.colors.bgSubtleSecondary,
errorContainerColor = ElementTheme.colors.bgSubtleSecondary,
)
) {
androidx.compose.material3.OutlinedTextField(
androidx.compose.material3.TextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
@ -83,7 +81,6 @@ fun OutlinedTextField(
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
interactionSource = interactionSource,
shape = shape,
colors = colors,
@ -91,7 +88,7 @@ fun OutlinedTextField(
}
@Composable
fun OutlinedTextField(
fun FilledTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
@ -108,12 +105,12 @@ fun OutlinedTextField(
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = Int.MAX_VALUE,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = OutlinedTextFieldDefaults.shape,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors()
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors()
) {
androidx.compose.material3.OutlinedTextField(
androidx.compose.material3.TextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
@ -137,37 +134,62 @@ fun OutlinedTextField(
)
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.onTabOrEnterKeyFocusNext(focusManager: FocusManager): Modifier = onPreviewKeyEvent { event ->
if (event.key == Key.Tab || event.key == Key.Enter) {
if (event.type == KeyEventType.KeyUp) {
focusManager.moveFocus(FocusDirection.Down)
}
true
} else {
false
}
}
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun FilledTextFieldLightPreview() =
ElementPreviewLight { ContentToPreview() }
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun OutlinedTextFieldsPreview() = ElementPreviewLight { ContentToPreview() }
internal fun FilledTextFieldDarkPreview() =
ElementPreviewDark { ContentToPreview() }
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun OutlinedTextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
@ExcludeFromCoverage
@Composable
private fun ContentToPreview() {
Column(modifier = Modifier.padding(4.dp)) {
allBooleans.forEach { isError ->
allBooleans.forEach { enabled ->
allBooleans.forEach { readonly ->
OutlinedTextField(
FilledTextField(
value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}",
onValueChange = {},
label = { Text(text = "label") },
isError = isError,
enabled = enabled,
readOnly = readonly,
)
Spacer(modifier = Modifier.height(2.dp))
}
}
}
}
}
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun FilledTextFieldValueLightPreview() =
ElementPreviewLight { FilledTextFieldValueContentToPreview() }
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun FilledTextFieldValueTextFieldDarkPreview() =
ElementPreviewDark { FilledTextFieldValueContentToPreview() }
@ExcludeFromCoverage
@Composable
private fun FilledTextFieldValueContentToPreview() {
Column(modifier = Modifier.padding(4.dp)) {
allBooleans.forEach { isError ->
allBooleans.forEach { enabled ->
allBooleans.forEach { readonly ->
FilledTextField(
value = TextFieldValue(
text = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}",
selection = TextRange(0, "Hello".length),
),
onValueChange = {},
label = { Text(text = "label") },
value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}",
isError = isError,
enabled = enabled,
readOnly = readonly,

View file

@ -7,36 +7,37 @@
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.material3.LocalTextStyle
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.AutofillNode
import androidx.compose.ui.autofill.AutofillType
import androidx.compose.ui.composed
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalAutofill
import androidx.compose.ui.platform.LocalAutofillTree
import androidx.compose.ui.text.TextRange
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
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
@ -44,56 +45,63 @@ 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 TextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
label: String? = null,
supportingText: String? = null,
placeholder: String? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
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() },
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors(
unfocusedContainerColor = ElementTheme.colors.bgSubtleSecondary,
focusedContainerColor = ElementTheme.colors.bgSubtleSecondary,
disabledContainerColor = ElementTheme.colors.bgSubtleSecondary,
errorContainerColor = ElementTheme.colors.bgSubtleSecondary,
)
onTextLayout: (TextLayoutResult) -> Unit = {},
) {
androidx.compose.material3.TextField(
val isFocused by interactionSource.collectIsFocusedAsState()
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
textStyle = textFieldStyle(enabled),
interactionSource = interactionSource,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle,
label = label,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
supportingText = supportingText,
isError = isError,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
interactionSource = interactionSource,
shape = shape,
colors = colors,
)
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.isEmpty(),
innerTextField = innerTextField,
trailingIcon = trailingIcon,
supportingText = supportingText
)
}
}
@Composable
@ -101,69 +109,200 @@ fun TextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
label: String? = null,
supportingText: String? = null,
placeholder: String? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
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() },
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors()
onTextLayout: (TextLayoutResult) -> Unit = {},
) {
androidx.compose.material3.TextField(
val isFocused by interactionSource.collectIsFocusedAsState()
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
textStyle = textFieldStyle(enabled),
interactionSource = interactionSource,
enabled = enabled,
readOnly = readOnly,
textStyle = textStyle,
label = label,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
supportingText = supportingText,
isError = isError,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
interactionSource = interactionSource,
shape = shape,
colors = colors,
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))
}
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 && isTextEmpty) {
Text(
text = placeholder,
color = ElementTheme.colors.textPlaceholder,
style = ElementTheme.typography.fontBodyLgRegular,
)
}
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))
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 TextFieldLightPreview() =
ElementPreviewLight { ContentToPreview() }
internal fun TextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun TextFieldDarkPreview() =
ElementPreviewDark { ContentToPreview() }
internal fun TextFieldsDarkPreview() = ElementPreviewDark { ContentToPreview() }
@ExcludeFromCoverage
@Composable
@ExcludeFromCoverage
private fun ContentToPreview() {
Column(modifier = Modifier.padding(4.dp)) {
allBooleans.forEach { isError ->
allBooleans.forEach { enabled ->
allBooleans.forEach { readonly ->
TextField(
onValueChange = {},
label = "Label",
value = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}",
onValueChange = {},
label = { Text(text = "label") },
supportingText = "Supporting text",
isError = isError,
enabled = enabled,
readOnly = readonly,
@ -174,62 +313,3 @@ private fun ContentToPreview() {
}
}
}
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun TextFieldValueLightPreview() =
ElementPreviewLight { TextFieldValueContentToPreview() }
@Preview(group = PreviewGroup.TextFields)
@Composable
internal fun TextFieldValueTextFieldDarkPreview() =
ElementPreviewDark { TextFieldValueContentToPreview() }
@ExcludeFromCoverage
@Composable
private fun TextFieldValueContentToPreview() {
Column(modifier = Modifier.padding(4.dp)) {
allBooleans.forEach { isError ->
allBooleans.forEach { enabled ->
allBooleans.forEach { readonly ->
TextField(
value = TextFieldValue(
text = "Hello er=${isError.asInt()}, en=${enabled.asInt()}, ro=${readonly.asInt()}",
selection = TextRange(0, "Hello".length),
),
onValueChange = {},
label = { Text(text = "label") },
isError = isError,
enabled = enabled,
readOnly = readonly,
)
Spacer(modifier = Modifier.height(2.dp))
}
}
}
}
}
@Suppress("ModifierComposed")
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.autofill(autofillTypes: List<AutofillType>, onFill: (String) -> Unit) = composed {
val autofillNode = AutofillNode(autofillTypes, onFill = onFill)
LocalAutofillTree.current += autofillNode
val autofill = LocalAutofill.current
this
.onGloballyPositioned {
// Inform autofill framework of where our composable is so it can show the popup in the right place
autofillNode.boundingBox = it.boundsInWindow()
}
.onFocusChanged {
autofill?.run {
if (it.isFocused) {
requestAutofillForNode(autofillNode)
} else {
cancelAutofillForNode(autofillNode)
}
}
}
}