Merge pull request #6765 from thundernest/add-textfields-to-design-system

Add textfields to design system
This commit is contained in:
Wolf-Martell Montwé 2023-03-21 10:06:50 +00:00 committed by GitHub
commit 3f8a3ff0c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 653 additions and 2 deletions

View file

@ -15,6 +15,7 @@ import app.k9mail.ui.catalog.items.buttonItems
import app.k9mail.ui.catalog.items.colorItems import app.k9mail.ui.catalog.items.colorItems
import app.k9mail.ui.catalog.items.imageItems import app.k9mail.ui.catalog.items.imageItems
import app.k9mail.ui.catalog.items.selectionControlItems import app.k9mail.ui.catalog.items.selectionControlItems
import app.k9mail.ui.catalog.items.textFieldItems
import app.k9mail.ui.catalog.items.themeHeaderItem import app.k9mail.ui.catalog.items.themeHeaderItem
import app.k9mail.ui.catalog.items.themeSelectorItems import app.k9mail.ui.catalog.items.themeSelectorItems
import app.k9mail.ui.catalog.items.typographyItems import app.k9mail.ui.catalog.items.typographyItems
@ -48,6 +49,7 @@ fun CatalogContent(
colorItems() colorItems()
buttonItems() buttonItems()
selectionControlItems() selectionControlItems()
textFieldItems()
imageItems() imageItems()
} }
} }

View file

@ -0,0 +1,90 @@
package app.k9mail.ui.catalog.items
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import app.k9mail.core.ui.compose.designsystem.atom.textfield.PasswordTextFieldOutlined
import app.k9mail.core.ui.compose.designsystem.atom.textfield.TextFieldOutlined
fun LazyGridScope.textFieldItems() {
sectionHeaderItem(text = "Text fields")
textFieldOutlinedItems()
passwordTextFieldOutlinedItems()
}
private fun LazyGridScope.textFieldOutlinedItems() {
sectionSubtitleItem(text = "Outlined")
item {
WithRememberedInput(text = "Initial text") { input ->
TextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
)
}
}
item {
WithRememberedInput(text = "Input text with error") { input ->
TextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
isError = true,
)
}
}
item {
WithRememberedInput(text = "Input text disabled") { input ->
TextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
enabled = false,
)
}
}
}
private fun LazyGridScope.passwordTextFieldOutlinedItems() {
sectionSubtitleItem(text = "Password outlined")
item {
WithRememberedInput(text = "Input text") { input ->
PasswordTextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
)
}
}
item {
WithRememberedInput(text = "Input text with error") { input ->
PasswordTextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
isError = true,
)
}
}
item {
WithRememberedInput(text = "Input text disabled") { input ->
PasswordTextFieldOutlined(
value = input.value,
label = "Label",
onValueChange = { input.value = it },
enabled = false,
)
}
}
}
@Composable
private fun WithRememberedInput(
text: String,
content: @Composable (text: MutableState<String>) -> Unit,
) {
val inputText = remember { mutableStateOf(text) }
content(inputText)
}

View file

@ -6,6 +6,13 @@ android {
configureSharedComposeConfig(libs) configureSharedComposeConfig(libs)
} }
androidComponents {
beforeVariants(selector().withBuildType("release")) { variantBuilder ->
variantBuilder.enableUnitTest = false
variantBuilder.enableAndroidTest = false
}
}
dependencies { dependencies {
configureSharedComposeDependencies(libs) configureSharedComposeDependencies(libs)
} }

View file

@ -4,10 +4,13 @@ plugins {
android { android {
namespace = "app.k9mail.core.ui.compose.designsystem" namespace = "app.k9mail.core.ui.compose.designsystem"
resourcePrefix = "core_ui_designsystem_" resourcePrefix = "designsystem_"
} }
dependencies { dependencies {
api(projects.core.ui.compose.theme) api(projects.core.ui.compose.theme)
implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material.icons.extended)
testImplementation(projects.core.ui.compose.testing)
} }

View file

@ -31,3 +31,15 @@ internal fun CheckboxPreview() {
) )
} }
} }
@Preview(showBackground = true)
@Composable
internal fun CheckboxDisabledPreview() {
PreviewWithThemes {
Checkbox(
checked = true,
onCheckedChange = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,154 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.R
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedTextField as MaterialOutlinedTextField
@Composable
fun PasswordTextFieldOutlined(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: String? = null,
isError: Boolean = false,
) {
var passwordVisibilityState by rememberSaveable { mutableStateOf(false) }
MaterialOutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = selectLabel(label),
trailingIcon = selectTrailingIcon(
isEnabled = enabled,
isPasswordVisible = passwordVisibilityState,
onClick = { passwordVisibilityState = !passwordVisibilityState },
),
isError = isError,
visualTransformation = selectVisualTransformation(
isEnabled = enabled,
isPasswordVisible = passwordVisibilityState,
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
)
}
private fun selectLabel(label: String?): @Composable (() -> Unit)? {
return if (label != null) {
{
Text(text = label)
}
} else {
null
}
}
private fun selectTrailingIcon(
isEnabled: Boolean,
isPasswordVisible: Boolean,
onClick: () -> Unit,
hasTrailingIcon: Boolean = true,
): @Composable (() -> Unit)? {
return if (hasTrailingIcon) {
{
val image = if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
Icons.Filled.Visibility
} else {
Icons.Filled.VisibilityOff
}
val description = if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
stringResource(id = R.string.designsystem_atom_password_textfield_hide_password)
} else {
stringResource(id = R.string.designsystem_atom_password_textfield_show_password)
}
IconButton(onClick = onClick) {
Icon(imageVector = image, contentDescription = description)
}
}
} else {
null
}
}
private fun selectVisualTransformation(
isEnabled: Boolean,
isPasswordVisible: Boolean,
): VisualTransformation {
return if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
}
}
private fun isShowPasswordAllowed(isEnabled: Boolean, isPasswordVisible: Boolean) = isEnabled && isPasswordVisible
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedWithLabelPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
label = "Label",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedDisabledPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
enabled = false,
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedErrorPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
isError = true,
)
}
}

View file

@ -0,0 +1,84 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedTextField as MaterialOutlinedTextField
@Composable
fun TextFieldOutlined(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: String? = null,
isError: Boolean = false,
) {
MaterialOutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = selectLabel(label),
isError = isError,
)
}
private fun selectLabel(label: String?): @Composable (() -> Unit)? {
return if (label != null) {
{
Text(text = label)
}
} else {
null
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedWithLabelPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
label = "Label",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedDisabledPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
enabled = false,
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedErrorPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
isError = true,
)
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="designsystem_atom_password_textfield_hide_password">Hide password</string>
<string name="designsystem_atom_password_textfield_show_password">Show password</string>
</resources>

View file

@ -0,0 +1,98 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import app.k9mail.core.ui.compose.designsystem.R
import app.k9mail.core.ui.compose.testing.ComposeTest
import org.junit.Test
private const val PASSWORD = "Password input"
class PasswordTextFieldOutlinedKtTest : ComposeTest() {
@Test
fun `should not display password by default`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onNodeWithText(PASSWORD).assertDoesNotExist()
}
@Test
fun `should display password when show password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onNodeWithText(PASSWORD).assertIsDisplayed()
}
@Test
fun `should not display password when hide password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().performClick()
onNodeWithText(PASSWORD).assertDoesNotExist()
}
@Test
fun `should display hide password icon when show password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().assertIsDisplayed()
}
@Test
fun `should display show password icon when hide password icon is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().performClick()
onShowPasswordNode().assertIsDisplayed()
}
private fun SemanticsNodeInteractionsProvider.onShowPasswordNode(): SemanticsNodeInteraction {
return onNodeWithContentDescription(
getString(R.string.designsystem_atom_password_textfield_show_password),
)
}
private fun SemanticsNodeInteractionsProvider.onHidePasswordNode(): SemanticsNodeInteraction {
return onNodeWithContentDescription(
getString(R.string.designsystem_atom_password_textfield_hide_password),
)
}
}

View file

@ -0,0 +1,154 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
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
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
private const val VALUE = "Input text"
private const val LABEL = "Label"
data class TextFieldTestData(
val name: String,
val content: @Composable (
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier,
enabled: Boolean?,
label: String?,
) -> Unit,
)
@RunWith(ParameterizedRobolectricTestRunner::class)
class TextFieldKtTest(
data: TextFieldTestData,
) : ComposeTest() {
private val testSubjectName = data.name
private val testSubject = data.content
@Test
fun `should call onValueChange when value changes`() = runComposeTest {
var value = VALUE
setContent {
testSubject(
value = value,
onValueChange = { value = it },
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = null,
)
}
onNodeWithTag(testSubjectName).performClick()
onNodeWithTag(testSubjectName).performTextInput(" + added text")
assertThat(value).isEqualTo("$VALUE + added text")
}
@Test
fun `should be enabled by default`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = null,
)
}
onNodeWithTag(testSubjectName).assertIsEnabled()
}
@Test
fun `should be disabled when enabled is false`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = false,
label = null,
)
}
onNodeWithTag(testSubjectName).assertIsNotEnabled()
}
@Test
fun `should show label when label is not null`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = LABEL,
)
}
onNodeWithText(LABEL).assertIsDisplayed()
}
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun data(): List<TextFieldTestData> = listOf(
TextFieldTestData(
name = "TextFieldOutlined",
content = { value, onValueChange, modifier, enabled, label ->
if (enabled != null) {
TextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = label,
)
} else {
TextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label,
)
}
},
),
TextFieldTestData(
name = "PasswordTextFieldOutlined",
content = { value, onValueChange, modifier, enabled, label ->
if (enabled != null) {
PasswordTextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = label,
)
} else {
PasswordTextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label,
)
}
},
),
)
}
}

View file

@ -0,0 +1,3 @@
## Core - UI - Compose - Testing
Uses [`:core:ui:compose:theme`](../theme/README.md)

View file

@ -0,0 +1,14 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "app.k9mail.core.ui.compose.testing"
}
dependencies {
implementation(projects.core.ui.compose.theme)
implementation(libs.androidx.compose.material)
implementation(libs.bundles.shared.jvm.test.compose)
}

View file

@ -0,0 +1,22 @@
package app.k9mail.core.ui.compose.testing
import androidx.annotation.StringRes
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
open class ComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
fun getString(@StringRes resourceId: Int): String = RuntimeEnvironment.getApplication().getString(resourceId)
fun runComposeTest(testContent: ComposeContentTestRule.() -> Unit): Unit = with(composeTestRule) {
testContent()
}
}

View file

@ -21,6 +21,7 @@ androidxDrawerLayout = "1.1.1"
androidxTransition = "1.4.1" androidxTransition = "1.4.1"
androidxComposeCompiler = "1.4.1" androidxComposeCompiler = "1.4.1"
androidxComposeBom = "2023.01.00" androidxComposeBom = "2023.01.00"
androidxComposeMaterial = "1.3.1"
fastAdapter = "5.7.0" fastAdapter = "5.7.0"
preferencesFix = "1.1.0" preferencesFix = "1.1.0"
timber = "5.0.1" timber = "5.0.1"
@ -80,7 +81,8 @@ androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-mani
androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-compose-activity = "androidx.activity:activity-compose:1.6.1" androidx-compose-activity = "androidx.activity:activity-compose:1.6.1"
androidx-compose-lifecycle-viewmodel = "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1" androidx-compose-lifecycle-viewmodel = "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1"
androidx-compose-material = "androidx.compose.material:material:1.3.1" androidx-compose-material = { module = "androidx.compose.material:material", version.ref = "androidxComposeMaterial" }
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "androidxComposeMaterial" }
androidx-test-core = "androidx.test:core:1.5.0" androidx-test-core = "androidx.test:core:1.5.0"
androidx-test-ext-junit-ktx = "androidx.test.ext:junit-ktx:1.1.5" androidx-test-ext-junit-ktx = "androidx.test.ext:junit-ktx:1.1.5"
androidx-test-espresso-core = "androidx.test.espresso:espresso-core:3.5.1" androidx-test-espresso-core = "androidx.test.espresso:espresso-core:3.5.1"

View file

@ -49,6 +49,7 @@ include(
":core:ui:compose:common", ":core:ui:compose:common",
":core:ui:compose:designsystem", ":core:ui:compose:designsystem",
":core:ui:compose:theme", ":core:ui:compose:theme",
":core:ui:compose:testing",
) )
include( include(