Merge pull request #6948 from thundernest/add_number_input
Add TextFieldOutlinedNumber atom and NumberInput molecule to the design system
This commit is contained in:
commit
f99f6309ae
11 changed files with 325 additions and 6 deletions
|
@ -16,6 +16,7 @@ import app.k9mail.core.ui.compose.designsystem.atom.Icon
|
||||||
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
|
import app.k9mail.core.ui.compose.designsystem.atom.text.TextSubtitle1
|
||||||
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlined
|
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlined
|
||||||
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedEmailAddress
|
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedEmailAddress
|
||||||
|
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedNumber
|
||||||
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedPassword
|
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedPassword
|
||||||
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedSelect
|
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlinedSelect
|
||||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.CheckboxInput
|
import app.k9mail.core.ui.compose.designsystem.molecule.input.CheckboxInput
|
||||||
|
@ -36,6 +37,8 @@ fun LazyGridScope.textFieldItems() {
|
||||||
passwordTextFieldOutlinedItems()
|
passwordTextFieldOutlinedItems()
|
||||||
sectionSubtitleItem(text = "Email address")
|
sectionSubtitleItem(text = "Email address")
|
||||||
emailTextFieldOutlinedItems()
|
emailTextFieldOutlinedItems()
|
||||||
|
sectionSubtitleItem(text = "Number")
|
||||||
|
numberTextFieldOutlinedItems()
|
||||||
sectionSubtitleItem(text = "Selection")
|
sectionSubtitleItem(text = "Selection")
|
||||||
selectionTextFieldOutlinedItems()
|
selectionTextFieldOutlinedItems()
|
||||||
}
|
}
|
||||||
|
@ -215,6 +218,28 @@ private fun LazyGridScope.emailTextFieldOutlinedItems() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun LazyGridScope.numberTextFieldOutlinedItems() {
|
||||||
|
item {
|
||||||
|
TextFieldDemo(
|
||||||
|
initialState = TextFieldState<Long?>(
|
||||||
|
input = 123L,
|
||||||
|
label = "Number",
|
||||||
|
),
|
||||||
|
) { state ->
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = state.value.input,
|
||||||
|
label = if (state.value.showLabel) state.value.label else null,
|
||||||
|
onValueChange = { state.value = state.value.copy(input = it) },
|
||||||
|
isEnabled = !state.value.isDisabled,
|
||||||
|
isReadOnly = state.value.isReadOnly,
|
||||||
|
isRequired = state.value.isRequired,
|
||||||
|
hasError = state.value.hasError,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private data class TextFieldSelectState(
|
private data class TextFieldSelectState(
|
||||||
val options: ImmutableList<String> = persistentListOf("Option 1", "Option 2", "Option 3"),
|
val options: ImmutableList<String> = persistentListOf("Option 1", "Option 2", "Option 3"),
|
||||||
val selectedOption: String = options.first(),
|
val selectedOption: String = options.first(),
|
||||||
|
|
|
@ -3,6 +3,7 @@ package app.k9mail.ui.catalog.ui.molecule.items
|
||||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.CheckboxInput
|
import app.k9mail.core.ui.compose.designsystem.molecule.input.CheckboxInput
|
||||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.EmailAddressInput
|
import app.k9mail.core.ui.compose.designsystem.molecule.input.EmailAddressInput
|
||||||
|
import app.k9mail.core.ui.compose.designsystem.molecule.input.NumberInput
|
||||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.PasswordInput
|
import app.k9mail.core.ui.compose.designsystem.molecule.input.PasswordInput
|
||||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput
|
import app.k9mail.core.ui.compose.designsystem.molecule.input.SelectInput
|
||||||
import app.k9mail.core.ui.compose.designsystem.molecule.input.SwitchInput
|
import app.k9mail.core.ui.compose.designsystem.molecule.input.SwitchInput
|
||||||
|
@ -65,6 +66,31 @@ fun LazyGridScope.inputItems() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sectionHeaderItem(text = "NumberInput")
|
||||||
|
sectionSubtitleItem(text = "Default")
|
||||||
|
item {
|
||||||
|
ItemOutlined {
|
||||||
|
WithRememberedState<Long?>(input = null) { state ->
|
||||||
|
NumberInput(
|
||||||
|
value = state.value,
|
||||||
|
onValueChange = { state.value = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sectionSubtitleItem(text = "With error")
|
||||||
|
item {
|
||||||
|
ItemOutlined {
|
||||||
|
WithRememberedState<Long?>(input = 123L) { state ->
|
||||||
|
NumberInput(
|
||||||
|
value = state.value,
|
||||||
|
onValueChange = { state.value = it },
|
||||||
|
errorMessage = "Invalid number",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sectionHeaderItem(text = "PasswordInput")
|
sectionHeaderItem(text = "PasswordInput")
|
||||||
sectionSubtitleItem(text = "Default")
|
sectionSubtitleItem(text = "Default")
|
||||||
item {
|
item {
|
||||||
|
|
|
@ -39,7 +39,7 @@ fun TextFieldOutlinedEmailAddress(
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextFieldOutlinedEmailAddressPreview() {
|
internal fun TextFieldOutlinedEmailAddressPreview() {
|
||||||
PreviewWithThemes {
|
PreviewWithThemes {
|
||||||
TextFieldOutlined(
|
TextFieldOutlinedEmailAddress(
|
||||||
value = "Input text",
|
value = "Input text",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
)
|
)
|
||||||
|
@ -74,7 +74,7 @@ internal fun TextFieldOutlinedEmailDisabledPreview() {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextFieldOutlinedEmailErrorPreview() {
|
internal fun TextFieldOutlinedEmailErrorPreview() {
|
||||||
PreviewWithThemes {
|
PreviewWithThemes {
|
||||||
TextFieldOutlined(
|
TextFieldOutlinedEmailAddress(
|
||||||
value = "Input text",
|
value = "Input text",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
hasError = true,
|
hasError = true,
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package app.k9mail.core.ui.compose.designsystem.atom.textfield
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
|
||||||
|
import androidx.compose.material.OutlinedTextField as MaterialOutlinedTextField
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun TextFieldOutlinedNumber(
|
||||||
|
value: Long?,
|
||||||
|
onValueChange: (Long?) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
label: String? = null,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
|
isReadOnly: Boolean = false,
|
||||||
|
isRequired: Boolean = false,
|
||||||
|
hasError: Boolean = false,
|
||||||
|
) {
|
||||||
|
MaterialOutlinedTextField(
|
||||||
|
value = value?.toString() ?: "",
|
||||||
|
onValueChange = {
|
||||||
|
onValueChange(it.toLongOrNull())
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = isEnabled,
|
||||||
|
label = selectLabel(label, isRequired),
|
||||||
|
readOnly = isReadOnly,
|
||||||
|
isError = hasError,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.NumberPassword,
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun TextFieldOutlinedNumberPreview() {
|
||||||
|
PreviewWithThemes {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = 123L,
|
||||||
|
onValueChange = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun TextFieldOutlinedNumberWithLabelPreview() {
|
||||||
|
PreviewWithThemes {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = 123L,
|
||||||
|
label = "Label",
|
||||||
|
onValueChange = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun TextFieldOutlinedNumberDisabledPreview() {
|
||||||
|
PreviewWithThemes {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = 123L,
|
||||||
|
onValueChange = {},
|
||||||
|
isEnabled = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun TextFieldOutlinedNumberErrorPreview() {
|
||||||
|
PreviewWithThemes {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = 123L,
|
||||||
|
onValueChange = {},
|
||||||
|
hasError = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ fun EmailAddressInput(
|
||||||
errorMessage: String? = null,
|
errorMessage: String? = null,
|
||||||
contentPadding: PaddingValues = inputContentPadding(),
|
contentPadding: PaddingValues = inputContentPadding(),
|
||||||
) {
|
) {
|
||||||
TextInputLayout(
|
InputLayout(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
errorMessage = errorMessage,
|
errorMessage = errorMessage,
|
||||||
|
|
|
@ -11,7 +11,7 @@ import app.k9mail.core.ui.compose.designsystem.atom.text.TextCaption
|
||||||
import app.k9mail.core.ui.compose.theme.MainTheme
|
import app.k9mail.core.ui.compose.theme.MainTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TextInputLayout(
|
internal fun InputLayout(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentPadding: PaddingValues = inputContentPadding(),
|
contentPadding: PaddingValues = inputContentPadding(),
|
||||||
errorMessage: String? = null,
|
errorMessage: String? = null,
|
|
@ -0,0 +1,73 @@
|
||||||
|
package app.k9mail.core.ui.compose.designsystem.molecule.input
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.textfield.TextFieldOutlinedNumber
|
||||||
|
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun NumberInput(
|
||||||
|
onValueChange: (Long?) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
value: Long? = null,
|
||||||
|
label: String? = null,
|
||||||
|
isRequired: Boolean = false,
|
||||||
|
errorMessage: String? = null,
|
||||||
|
contentPadding: PaddingValues = inputContentPadding(),
|
||||||
|
) {
|
||||||
|
val inputError = remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
InputLayout(
|
||||||
|
modifier = modifier,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
errorMessage = errorMessage ?: inputError.value,
|
||||||
|
) {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
label = label,
|
||||||
|
isRequired = isRequired,
|
||||||
|
hasError = errorMessage != null,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun IntegerInputPreview() {
|
||||||
|
PreviewWithThemes {
|
||||||
|
NumberInput(
|
||||||
|
onValueChange = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun IntegerInputIsRequiredPreview() {
|
||||||
|
PreviewWithThemes {
|
||||||
|
NumberInput(
|
||||||
|
onValueChange = {},
|
||||||
|
label = "Text input is required",
|
||||||
|
isRequired = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
internal fun IntegerInputWithErrorPreview() {
|
||||||
|
PreviewWithThemes {
|
||||||
|
NumberInput(
|
||||||
|
onValueChange = {},
|
||||||
|
errorMessage = "Text input error",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ fun PasswordInput(
|
||||||
errorMessage: String? = null,
|
errorMessage: String? = null,
|
||||||
contentPadding: PaddingValues = inputContentPadding(),
|
contentPadding: PaddingValues = inputContentPadding(),
|
||||||
) {
|
) {
|
||||||
TextInputLayout(
|
InputLayout(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
errorMessage = errorMessage,
|
errorMessage = errorMessage,
|
||||||
|
|
|
@ -20,7 +20,7 @@ fun TextInput(
|
||||||
contentPadding: PaddingValues = inputContentPadding(),
|
contentPadding: PaddingValues = inputContentPadding(),
|
||||||
isSingleLine: Boolean = true,
|
isSingleLine: Boolean = true,
|
||||||
) {
|
) {
|
||||||
TextInputLayout(
|
InputLayout(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
errorMessage = errorMessage,
|
errorMessage = errorMessage,
|
||||||
|
|
|
@ -227,6 +227,31 @@ class CommonTextFieldTest(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
CommonTextFieldTestData(
|
||||||
|
name = "TextFieldOutlinedNumber",
|
||||||
|
content = { modifier, config ->
|
||||||
|
if (config.isEnabled != null) {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = 123L,
|
||||||
|
onValueChange = {},
|
||||||
|
modifier = modifier,
|
||||||
|
label = config.label,
|
||||||
|
isEnabled = config.isEnabled,
|
||||||
|
isReadOnly = config.isReadOnly,
|
||||||
|
isRequired = config.isRequired,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = 123L,
|
||||||
|
onValueChange = {},
|
||||||
|
modifier = modifier,
|
||||||
|
label = config.label,
|
||||||
|
isRequired = config.isRequired,
|
||||||
|
isReadOnly = config.isReadOnly,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
CommonTextFieldTestData(
|
CommonTextFieldTestData(
|
||||||
name = "TextFieldOutlinedSelect",
|
name = "TextFieldOutlinedSelect",
|
||||||
content = { modifier, config ->
|
content = { modifier, config ->
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package app.k9mail.core.ui.compose.designsystem.atom.textfield
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performTextClearance
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import app.k9mail.core.ui.compose.testing.ComposeTest
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isEqualTo
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
private const val TEST_TAG = "TextFieldOutlinedNumber"
|
||||||
|
|
||||||
|
class TextFieldOutlinedNumberKtTest : ComposeTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should call onValueChange with null when input is empty`() = runComposeTest {
|
||||||
|
var value: Long? = 1L
|
||||||
|
setContent {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { value = it },
|
||||||
|
modifier = Modifier.testTag(TEST_TAG),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TEST_TAG).performClick()
|
||||||
|
onNodeWithTag(TEST_TAG).performTextClearance()
|
||||||
|
|
||||||
|
assertThat(value).isEqualTo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should call onValueChange when value changes`() = runComposeTest {
|
||||||
|
var value: Long? = 123L
|
||||||
|
setContent {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { value = it },
|
||||||
|
modifier = Modifier.testTag(TEST_TAG),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TEST_TAG).performClick()
|
||||||
|
onNodeWithTag(TEST_TAG).performTextInput("456")
|
||||||
|
|
||||||
|
assertThat(value).isEqualTo(123456L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return null when no number`() = runComposeTest {
|
||||||
|
var value: Long? = 123L
|
||||||
|
setContent {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { value = it },
|
||||||
|
modifier = Modifier.testTag(TEST_TAG),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TEST_TAG).performClick()
|
||||||
|
onNodeWithTag(TEST_TAG).performTextInput(",")
|
||||||
|
|
||||||
|
assertThat(value).isEqualTo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `should return null when input exceeds max long`() = runComposeTest {
|
||||||
|
var value: Long? = null
|
||||||
|
setContent {
|
||||||
|
TextFieldOutlinedNumber(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { value = it },
|
||||||
|
modifier = Modifier.testTag(TEST_TAG),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNodeWithTag(TEST_TAG).performClick()
|
||||||
|
onNodeWithTag(TEST_TAG).performTextInput("9223372036854775808")
|
||||||
|
|
||||||
|
assertThat(value).isEqualTo(null)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue