Add TextFieldOutlinedSelect for selecting entries from a set of options

This commit is contained in:
Wolf-Martell Montwé 2023-05-23 09:48:17 +02:00
parent 520dc4049c
commit 7d18529e73
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
3 changed files with 185 additions and 2 deletions

View file

@ -0,0 +1,141 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExposedDropdownMenuBox
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.atom.Icon
import app.k9mail.core.ui.compose.theme.Icons
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Suppress("LongParameterList")
@Composable
fun <T> TextFieldOutlinedSelect(
options: ImmutableList<T>,
selectedOption: T,
onValueChange: (T) -> Unit,
modifier: Modifier = Modifier,
label: String? = null,
isEnabled: Boolean = true,
isReadOnly: Boolean = false,
isRequired: Boolean = false,
hasError: Boolean = false,
) {
TextFieldDropDownWrapper(
isReadOnlyOrDisabled = isReadOnly || !isEnabled,
options = options,
onValueChange = onValueChange,
) { expanded ->
TextFieldOutlined(
value = selectedOption.toString(),
onValueChange = { },
label = label,
trailingIcon = selectTrailingIcon(expanded),
isEnabled = isEnabled,
isReadOnly = true,
isRequired = isRequired,
hasError = hasError,
modifier = Modifier
.fillMaxWidth()
.then(modifier),
)
}
}
private fun selectTrailingIcon(
isExpanded: Boolean,
): @Composable () -> Unit {
return if (isExpanded) {
{ Icon(Icons.Outlined.arrowDropUp) }
} else {
{ Icon(Icons.Outlined.arrowDropDown) }
}
}
@Composable
private fun <T> TextFieldDropDownWrapper(
isReadOnlyOrDisabled: Boolean,
options: ImmutableList<T>,
onValueChange: (T) -> Unit,
content: @Composable (expanded: Boolean) -> Unit,
) {
if (isReadOnlyOrDisabled) {
content(false)
} else {
DropDownMenu(
options = options,
onValueChange = onValueChange,
content = content,
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun <T> DropDownMenu(
options: ImmutableList<T>,
onValueChange: (T) -> Unit,
content: @Composable (expanded: Boolean) -> Unit,
) {
var expanded = remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded.value,
onExpandedChange = {
expanded.value = expanded.value.not()
},
) {
content(expanded.value)
ExposedDropdownMenu(
expanded = expanded.value,
onDismissRequest = {
expanded.value = false
},
) {
options.forEach { option ->
DropdownMenuItem(
onClick = {
onValueChange(option)
expanded.value = false
},
) {
Text(text = option.toString())
}
}
}
}
}
@Preview
@Composable
internal fun TextFieldOutlinedSelectPreview() {
PreviewWithThemes {
TextFieldOutlinedSelect(
options = persistentListOf("Option 1", "Option 2", "Option 3"),
selectedOption = "Option 1",
onValueChange = {},
)
}
}
@Preview
@Composable
internal fun TextFieldOutlinedSelectPreviewWithLabel() {
PreviewWithThemes {
TextFieldOutlinedSelect(
options = persistentListOf("Option 1", "Option 2", "Option 3"),
selectedOption = "Option 1",
onValueChange = {},
label = "Label",
)
}
}

View file

@ -14,6 +14,8 @@ import app.k9mail.core.ui.compose.testing.ComposeTest
import assertk.assertFailure
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
@ -36,6 +38,7 @@ data class TextFieldTestData<INPUT, VALUE>(
modifier: Modifier,
textFieldConfig: TextFieldConfig,
) -> Unit,
val isSelectionTest: Boolean = false,
)
@RunWith(ParameterizedRobolectricTestRunner::class)
@ -46,6 +49,7 @@ class TextFieldKtTest(
private val testSubjectName = data.name
private val testSubject = data.content
private val testInput = data.input
private val isSelectionTest = data.isSelectionTest
@Test
fun `should call onValueChange when value changes`() = runComposeTest {
@ -65,9 +69,14 @@ class TextFieldKtTest(
}
onNodeWithTag(testSubjectName).performClick()
onNodeWithTag(testSubjectName).performTextInput(" + added text")
assertThat(value).isEqualTo("$testInput + added text")
if (isSelectionTest) {
onNodeWithText((testInput as ImmutableList<*>)[1].toString()).performClick()
assertThat(value).isEqualTo(testInput[1])
} else {
onNodeWithTag(testSubjectName).performTextInput(" + added text")
assertThat(value).isEqualTo("$testInput + added text")
}
}
@Test
@ -270,6 +279,37 @@ class TextFieldKtTest(
}
},
),
TextFieldTestData(
name = "TextFieldOutlinedSelect",
input = persistentListOf("option1", "option2"),
isSelectionTest = true,
content = { value, onValueChange: (Any) -> Unit, modifier, config ->
if (config.isEnabled != null) {
@Suppress("UNCHECKED_CAST")
TextFieldOutlinedSelect(
options = value as ImmutableList<Any>,
selectedOption = value.first(),
onValueChange = onValueChange,
modifier = modifier,
label = config.label,
isEnabled = config.isEnabled,
isReadOnly = config.isReadOnly,
isRequired = config.isRequired,
)
} else {
@Suppress("UNCHECKED_CAST")
TextFieldOutlinedSelect(
options = value as ImmutableList<Any>,
selectedOption = value.first(),
onValueChange = onValueChange,
modifier = modifier,
label = config.label,
isRequired = config.isRequired,
isReadOnly = config.isReadOnly,
)
}
},
),
)
}
}

View file

@ -57,6 +57,7 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
kotlinx-collections-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5"
jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrainsAnnotations" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppCompat" }
androidx-activity = { module = "androidx.activity:activity", version.ref = "androidxActivity" }
@ -166,6 +167,7 @@ shared-jvm-android-app = [
"android-material",
]
shared-jvm-android-compose = [
"kotlinx-collections-immutable",
"androidx-compose-foundation",
"androidx-compose-ui-tooling-preview",
"androidx-compose-lifecycle-runtime",