From 624fcb6f413ff3ee93f12059ac3be8c23a9fbd59 Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 6 Sep 2023 10:00:38 +0200 Subject: [PATCH 1/3] add Tags component --- .../kotlin/org/hisp/dhis/common/App.kt | 2 + .../hisp/dhis/common/screens/Components.kt | 1 + .../hisp/dhis/common/screens/TagsScreen.kt | 12 +++++ .../common/screens/previews/TagsPreview.kt | 17 ++++++ .../mobile/ui/designsystem/component/Tags.kt | 53 +++++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index 6c9b7a96d..dee52931c 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -34,6 +34,7 @@ import org.hisp.dhis.common.screens.LegendScreen import org.hisp.dhis.common.screens.ProgressScreen import org.hisp.dhis.common.screens.RadioButtonScreen import org.hisp.dhis.common.screens.SupportingTextScreen +import org.hisp.dhis.common.screens.TagsScreen import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing @@ -104,6 +105,7 @@ fun Main() { Components.FORM_SHELLS -> FormShellsScreen() Components.BUTTON_BLOCK -> ButtonBlockScreen() Components.BOTTOM_SHEET -> BottomSheetScreen() + Components.TAGS -> TagsScreen() } } } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt index 3d7d82ad1..ebb91429c 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt @@ -15,4 +15,5 @@ enum class Components(val label: String) { INPUT_TEXT("Input Text"), FORM_SHELLS("Form Shells"), BOTTOM_SHEET("Bottom Sheet"), + TAGS("Tags") } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt new file mode 100644 index 000000000..016df26ae --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt @@ -0,0 +1,12 @@ +package org.hisp.dhis.common.screens + +import androidx.compose.runtime.Composable +import org.hisp.dhis.common.screens.previews.TagsPreview +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer + +@Composable +fun TagsScreen() { + ColumnComponentContainer(title = "Tags"){ + TagsPreview() + } +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt new file mode 100644 index 000000000..124e078a4 --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt @@ -0,0 +1,17 @@ +package org.hisp.dhis.common.screens.previews + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.Tag +import org.hisp.dhis.mobile.ui.designsystem.component.TagType + +@Composable +fun TagsPreview() { + Column(verticalArrangement = spacedBy(20.dp)) { + TagType.values().forEach { + Tag(label = "label", type = it) + } + } +} \ No newline at end of file diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt new file mode 100644 index 000000000..8f9e73a29 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt @@ -0,0 +1,53 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import org.hisp.dhis.mobile.ui.designsystem.theme.Shape +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +enum class TagType { + ERROR, + WARNING, + DEFAULT +} + +@Composable +fun Tag( + modifier: Modifier = Modifier, + label: String, + type: TagType, +) { + Box( + modifier = modifier + .wrapContentSize() + .background( + color = when (type) { + TagType.ERROR -> SurfaceColor.ErrorContainer + TagType.WARNING -> SurfaceColor.WarningContainer + TagType.DEFAULT -> SurfaceColor.PrimaryContainer + }, + shape = Shape.ExtraSmall + ).padding(horizontal = Spacing.Spacing8) + ) { + Text( + modifier = Modifier.wrapContentSize(), + text = label, + style = MaterialTheme.typography.labelLarge, + color = when (type) { + TagType.ERROR -> TextColor.OnErrorContainer + TagType.WARNING -> TextColor.OnWarningContainer + TagType.DEFAULT -> TextColor.OnPrimaryContainer + }, + ) + } +} \ No newline at end of file From 8b0b14eab4385296f4def299b9b1f91a9ef0280f Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 7 Sep 2023 11:12:40 +0200 Subject: [PATCH 2/3] ANDROAPP-5512 Section component --- .../kotlin/org/hisp/dhis/common/App.kt | 10 +- .../hisp/dhis/common/screens/Components.kt | 3 +- .../hisp/dhis/common/screens/SectionScreen.kt | 160 ++++++++++ .../hisp/dhis/common/screens/TagsScreen.kt | 4 +- .../common/screens/previews/LoremIpsum.kt | 8 + .../common/screens/previews/TagsPreview.kt | 2 +- .../mobile/ui/designsystem/resource/String.kt | 9 + .../ui/designsystem/component/Button.kt | 10 +- .../ui/designsystem/component/Sections.kt | 300 ++++++++++++++++++ .../designsystem/component/SupportingText.kt | 35 +- .../mobile/ui/designsystem/component/Tags.kt | 10 +- .../component/internal/Modifiers.kt | 28 ++ .../mobile/ui/designsystem/resource/String.kt | 3 + .../mobile/ui/designsystem/theme/Ripple.kt | 12 +- .../resources/values/strings_en.xml | 7 + .../mobile/ui/designsystem/resource/String.kt | 10 + 16 files changed, 583 insertions(+), 28 deletions(-) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/SectionScreen.kt create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/LoremIpsum.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/Modifiers.kt diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index dee52931c..8d9237c77 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -1,5 +1,6 @@ package org.hisp.dhis.common +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -19,6 +20,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import org.hisp.dhis.common.screens.BottomSheetScreen import org.hisp.dhis.common.screens.ButtonBlockScreen import org.hisp.dhis.common.screens.ButtonScreen @@ -33,6 +35,7 @@ import org.hisp.dhis.common.screens.LegendDescriptionScreen import org.hisp.dhis.common.screens.LegendScreen import org.hisp.dhis.common.screens.ProgressScreen import org.hisp.dhis.common.screens.RadioButtonScreen +import org.hisp.dhis.common.screens.SectionScreen import org.hisp.dhis.common.screens.SupportingTextScreen import org.hisp.dhis.common.screens.TagsScreen import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2Theme @@ -50,7 +53,11 @@ fun Main() { val currentScreen = remember { mutableStateOf(Components.FORM_SHELLS) } var expanded by remember { mutableStateOf(false) } - Column(modifier = Modifier.padding(Spacing.Spacing16)) { + Column( + modifier = Modifier + .background(Color.White) + .padding(Spacing.Spacing16), + ) { Box( modifier = Modifier .fillMaxWidth(), @@ -106,6 +113,7 @@ fun Main() { Components.BUTTON_BLOCK -> ButtonBlockScreen() Components.BOTTOM_SHEET -> BottomSheetScreen() Components.TAGS -> TagsScreen() + Components.SECTIONS -> SectionScreen() } } } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt index ebb91429c..f6e83ad65 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Components.kt @@ -15,5 +15,6 @@ enum class Components(val label: String) { INPUT_TEXT("Input Text"), FORM_SHELLS("Form Shells"), BOTTOM_SHEET("Bottom Sheet"), - TAGS("Tags") + TAGS("Tags"), + SECTIONS("Sections"), } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/SectionScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/SectionScreen.kt new file mode 100644 index 000000000..326ed24bb --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/SectionScreen.kt @@ -0,0 +1,160 @@ +package org.hisp.dhis.common.screens + +import androidx.compose.foundation.layout.Column +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 org.hisp.dhis.common.screens.previews.lorem +import org.hisp.dhis.common.screens.previews.lorem_medium +import org.hisp.dhis.common.screens.previews.lorem_short +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.InputText +import org.hisp.dhis.mobile.ui.designsystem.component.Section +import org.hisp.dhis.mobile.ui.designsystem.component.SectionState +import org.hisp.dhis.mobile.ui.designsystem.component.SubTitle + +@Composable +fun SectionScreen() { + ColumnComponentContainer(title = "Section Header") { + SubTitle("Collapsible header") + + Column { + Section( + title = "Section title", + description = null, + completedFields = 2, + totalFields = 3, + state = SectionState.CLOSE, + errorCount = 0, + warningCount = 0, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title", + description = lorem, + completedFields = 2, + totalFields = 3, + state = SectionState.CLOSE, + errorCount = 2, + warningCount = 1, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title", + description = lorem_short, + completedFields = 2, + totalFields = 3, + state = SectionState.CLOSE, + errorCount = 2, + warningCount = 1, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title", + description = lorem_medium, + completedFields = 2, + totalFields = 3, + state = SectionState.CLOSE, + errorCount = 0, + warningCount = 0, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title Section title Section title Section title Section title", + description = null, + completedFields = 2, + totalFields = 3, + state = SectionState.CLOSE, + errorCount = 0, + warningCount = 0, + content = { TestingFields() }, + onNextSection = { }, + ) + } + + SubTitle("Flat header") + Section( + title = "Section title", + description = null, + completedFields = 2, + totalFields = 3, + state = SectionState.FIXED, + errorCount = 0, + warningCount = 0, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title", + description = lorem, + completedFields = 2, + totalFields = 3, + state = SectionState.FIXED, + errorCount = 2, + warningCount = 1, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title", + description = lorem_short, + completedFields = 2, + totalFields = 3, + state = SectionState.FIXED, + errorCount = 0, + warningCount = 0, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title", + description = lorem_medium, + completedFields = 2, + totalFields = 3, + state = SectionState.FIXED, + errorCount = 0, + warningCount = 0, + content = { TestingFields() }, + onNextSection = { }, + ) + Section( + title = "Section title Section title Section title Section title Section title", + description = lorem_medium, + completedFields = 2, + totalFields = 3, + state = SectionState.FIXED, + errorCount = 0, + warningCount = 0, + content = { TestingFields() }, + onNextSection = { }, + ) + } +} + +@Composable +private fun TestingFields() { + var inputValue1: String by rememberSaveable { mutableStateOf("Input") } + var inputValue2: String by rememberSaveable { mutableStateOf("") } + var inputValue3: String by rememberSaveable { mutableStateOf("") } + InputText( + title = "Label", + inputText = inputValue1, + onValueChanged = { inputValue1 = it ?: "" }, + ) + InputText( + title = "Label", + inputText = inputValue2, + onValueChanged = { inputValue2 = it ?: "" }, + ) + InputText( + title = "Label", + inputText = inputValue3, + onValueChanged = { inputValue3 = it ?: "" }, + ) +} diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt index 016df26ae..86f4b8d0f 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/TagsScreen.kt @@ -6,7 +6,7 @@ import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer @Composable fun TagsScreen() { - ColumnComponentContainer(title = "Tags"){ + ColumnComponentContainer(title = "Tags") { TagsPreview() } -} \ No newline at end of file +} diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/LoremIpsum.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/LoremIpsum.kt new file mode 100644 index 000000000..e2a542463 --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/LoremIpsum.kt @@ -0,0 +1,8 @@ +package org.hisp.dhis.common.screens.previews + +const val lorem = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas dolor lacus, aliquam. Lorem ipsum dolor sit amet, consectetur adipiscing elit." +const val lorem_short = + "Lorem ipsum dolor sit amet" +const val lorem_medium = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt index 124e078a4..993a665d2 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/previews/TagsPreview.kt @@ -14,4 +14,4 @@ fun TagsPreview() { Tag(label = "label", type = it) } } -} \ No newline at end of file +} diff --git a/designsystem/src/androidMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt b/designsystem/src/androidMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt index 30e522dd0..250e1f022 100644 --- a/designsystem/src/androidMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt +++ b/designsystem/src/androidMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt @@ -10,3 +10,12 @@ actual fun provideStringResource(id: String): String { if (resourceId == 0) return id return context.getString(resourceId) } + +@Composable +actual fun provideQuantityStringResource(id: String, quantity: Int): String { + val appendToId = when (quantity) { + 1 -> "one" + else -> "other" + } + return provideStringResource("${id}_$appendToId").format(quantity) +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Button.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Button.kt index a150eb234..add2b4c7e 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Button.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Button.kt @@ -60,10 +60,9 @@ fun Button( icon: @Composable (() -> Unit)? = null, modifier: Modifier = Modifier, + paddingValues: PaddingValues = getPaddingValues(icon != null), onClick: () -> Unit, ) { - val paddingValues = getPaddingValues(icon != null) - when (style) { ButtonStyle.FILLED -> { val textColor = if (enabled) TextColor.OnPrimary else TextColor.OnDisabledSurface @@ -81,7 +80,7 @@ fun Button( text = text, textColor = textColor, icon = icon, - + paddingValues = paddingValues, ) } ButtonStyle.TEXT -> { @@ -99,6 +98,7 @@ fun Button( text = text, textColor = textColor, icon = icon, + paddingValues = paddingValues, ) } ButtonStyle.ELEVATED -> { @@ -138,6 +138,7 @@ fun Button( text = text, textColor = textColor, icon = icon, + paddingValues = paddingValues, ) } } @@ -212,9 +213,8 @@ private fun SimpleButton( icon: @Composable (() -> Unit)? = null, onClick: () -> Unit, + paddingValues: PaddingValues, ) { - val paddingValues = getPaddingValues(icon != null) - Button( onClick = { onClick() }, modifier = modifier.hoverPointerIcon(enabled), diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt new file mode 100644 index 000000000..4cc82d739 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt @@ -0,0 +1,300 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.internal.bottomBorder +import org.hisp.dhis.mobile.ui.designsystem.resource.provideQuantityStringResource +import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource +import org.hisp.dhis.mobile.ui.designsystem.theme.Color.Ash600 +import org.hisp.dhis.mobile.ui.designsystem.theme.Shape +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing16 +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing24 +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +enum class SectionState { + OPEN, CLOSE, FIXED; + + fun getNextState(): SectionState { + return when (this) { + OPEN -> CLOSE + CLOSE -> OPEN + FIXED -> FIXED + } + } +} + +@Composable +fun Section( + modifier: Modifier = Modifier, + isLastSection: Boolean = false, + title: String, + description: String?, + completedFields: Int, + totalFields: Int, + state: SectionState, + errorCount: Int, + warningCount: Int, + onNextSection: () -> Unit, + content: @Composable ColumnScope.() -> Unit, +) { + var sectionState by remember(state) { mutableStateOf(state) } + val bottomPadding = when (state) { + SectionState.FIXED -> Spacing.Spacing40 + else -> Spacing16 + } + + Column( + modifier = modifier + .fillMaxWidth() + .background(Color.White) + .run { + if (state != SectionState.FIXED) { + bottomBorder(1.dp, Ash600) + } else { + this + } + } + .padding(top = Spacing.Spacing8, bottom = bottomPadding), + verticalArrangement = spacedBy(Spacing24), + ) { + SectionHeader( + title = title, + description = description, + completedFields = completedFields, + totalFields = totalFields, + sectionState = sectionState, + errorCount = errorCount, + warningCount = warningCount, + onSectionClick = { + sectionState = sectionState.getNextState() + }, + ) + AnimatedVisibility( + visible = sectionState != SectionState.CLOSE, + enter = expandVertically(expandFrom = Alignment.CenterVertically), + exit = shrinkVertically(shrinkTowards = Alignment.CenterVertically), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = spacedBy(Spacing16), + ) { + content() + } + } + if (!isLastSection && sectionState == SectionState.OPEN) { + Button( + modifier = Modifier.align(Alignment.End), + style = ButtonStyle.TEXT, + text = provideStringResource("action_next"), + icon = { + Icon( + imageVector = Icons.Filled.ArrowForward, + contentDescription = "Icon Button", + ) + }, + onClick = onNextSection, + ) + } + } +} + +@Composable +fun SectionHeader( + modifier: Modifier = Modifier, + title: String, + description: String?, + completedFields: Int, + totalFields: Int, + sectionState: SectionState, + errorCount: Int, + warningCount: Int, + showFieldsLabel: String = provideStringResource("show_fields"), + hideFieldsLabel: String = provideStringResource("hide_fields"), + onSectionClick: () -> Unit, +) { + val sectionStateButtonLabel by remember(sectionState) { + derivedStateOf { + when (sectionState) { + SectionState.OPEN -> hideFieldsLabel + SectionState.CLOSE -> showFieldsLabel + SectionState.FIXED -> "" + } + } + } + + val sectionStateButtonIcon by remember(sectionState) { + derivedStateOf { + when (sectionState) { + SectionState.OPEN -> Icons.Filled.KeyboardArrowUp + SectionState.CLOSE -> Icons.Filled.KeyboardArrowDown + SectionState.FIXED -> null + } + } + } + + val interactionSource = remember { MutableInteractionSource() } + + Column( + modifier = modifier + .fillMaxWidth() + .background(color = Color.White, Shape.Small) + .clip(Shape.Small) + .clickable( + enabled = sectionState != SectionState.FIXED, + role = Role.Button, + interactionSource = interactionSource, + indication = rememberRipple( + color = SurfaceColor.Primary, + ), + ) { + onSectionClick() + } + .padding(vertical = Spacing.Spacing8), + ) { + Row( + verticalAlignment = Alignment.Top, + ) { + Column( + modifier = Modifier.weight(1f), + ) { + SectionTitle( + modifier.fillMaxWidth(), + title = title, + ) + description?.let { + SupportingText( + modifier = Modifier.padding(Spacing.Spacing0), + text = it, + maxLines = 2, + onNoInteraction = { + Pair(interactionSource, onSectionClick) + }, + ) + } + } + + CompletionLabel( + completedFields = completedFields, + totalFields = totalFields, + ) + } + + Spacer(modifier = Modifier.size(Spacing.Spacing8)) + Row( + modifier = Modifier.wrapContentHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(Spacing.Spacing8), + ) { + if (sectionState != SectionState.FIXED) { + StateIndicator( + label = sectionStateButtonLabel, + icon = sectionStateButtonIcon, + ) + } + if (errorCount > 0) { + Tag( + label = provideQuantityStringResource("error", errorCount), + type = TagType.ERROR, + ) + } + if (warningCount > 0) { + Tag( + label = provideQuantityStringResource("warning", warningCount), + type = TagType.WARNING, + ) + } + } + } +} + +@Composable +internal fun SectionTitle( + modifier: Modifier = Modifier, + title: String, +) { + Text( + modifier = modifier, + text = title, + style = MaterialTheme.typography.titleLarge, + color = TextColor.OnSurface, + ) +} + +@Composable +internal fun CompletionLabel( + modifier: Modifier = Modifier, + completedFields: Int, + totalFields: Int, +) { + Box( + modifier = modifier.padding( + start = Spacing.Spacing8, + top = Spacing.Spacing4, + bottom = Spacing.Spacing4, + ), + ) { + Text( + modifier = modifier, + text = "$completedFields/$totalFields", + style = MaterialTheme.typography.bodyMedium, + color = TextColor.OnSurfaceLight, + ) + } +} + +@Composable +internal fun StateIndicator(label: String, icon: ImageVector?) { + Row( + modifier = Modifier + .requiredHeight(20.dp) + .padding(end = Spacing.Spacing4), + horizontalArrangement = spacedBy(Spacing.Spacing8), + ) { + icon?.let { + Icon(imageVector = it, "Icon Button", tint = SurfaceColor.Primary) + } + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + color = SurfaceColor.Primary, + ) + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SupportingText.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SupportingText.kt index 13db2d23c..722cde520 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SupportingText.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SupportingText.kt @@ -1,6 +1,8 @@ package org.hisp.dhis.mobile.ui.designsystem.component import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText import androidx.compose.material.ripple.LocalRippleTheme @@ -10,14 +12,17 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.sp +import kotlinx.coroutines.launch import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.Ripple import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing @@ -39,9 +44,15 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor fun SupportingText( text: String, state: SupportingTextState = SupportingTextState.DEFAULT, + maxLines: Int = 1, showMoreText: String = provideStringResource("show_more"), showLessText: String = provideStringResource("show_less"), - modifier: Modifier = Modifier, + modifier: Modifier = Modifier.padding( + start = Spacing.Spacing16, + top = Spacing.Spacing4, + end = Spacing.Spacing16, + ), + onNoInteraction: (() -> Pair Unit>)? = null, ) { var isExpanded by remember { mutableStateOf(false) } val textLayoutResultState = remember { mutableStateOf(null) } @@ -56,6 +67,7 @@ fun SupportingText( ) } val seeMoreTag = "SEE_MORE" + val scope = rememberCoroutineScope() val textLayoutResult = textLayoutResultState.value LaunchedEffect(textLayoutResult) { @@ -63,7 +75,7 @@ fun SupportingText( when { !isExpanded && textLayoutResult.hasVisualOverflow -> { - val lastCharIndex = textLayoutResult.getLineEnd(0) + val lastCharIndex = textLayoutResult.getLineEnd(maxLines - 1) val adjustedText = text .substring(startIndex = 0, endIndex = lastCharIndex) .dropLast(showLessText.length + 5) @@ -90,6 +102,7 @@ fun SupportingText( isClickable = true } + isExpanded -> { annotatedText = buildAnnotatedString { val expandedText = "$text " @@ -119,19 +132,25 @@ fun SupportingText( CompositionLocalProvider(LocalRippleTheme provides Ripple.CustomDHISRippleTheme) { ClickableText( text = annotatedText, - maxLines = if (isExpanded) Int.MAX_VALUE else 1, + maxLines = if (isExpanded) Int.MAX_VALUE else maxLines, onTextLayout = { textLayoutResultState.value = it }, - onClick = { - position -> - val annotations = annotatedText.getStringAnnotations(seeMoreTag, start = position, end = position) + onClick = { position -> + val annotations = + annotatedText.getStringAnnotations(seeMoreTag, start = position, end = position) annotations.firstOrNull()?.let { if (isClickable) { isExpanded = !isExpanded } + } ?: onNoInteraction?.invoke()?.let { (interactionSource, action) -> + scope.launch { + action.invoke() + val pressInteraction = PressInteraction.Press(Offset.Zero) + interactionSource.emit(pressInteraction) + interactionSource.emit(PressInteraction.Release(pressInteraction)) + } } }, - modifier = modifier.animateContentSize() - .padding(start = Spacing.Spacing16, top = Spacing.Spacing4, end = Spacing.Spacing16), + modifier = modifier.animateContentSize(), ) } } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt index 8f9e73a29..86a20ecd1 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Tags.kt @@ -3,13 +3,11 @@ package org.hisp.dhis.mobile.ui.designsystem.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import org.hisp.dhis.mobile.ui.designsystem.theme.Shape import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @@ -18,7 +16,7 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor enum class TagType { ERROR, WARNING, - DEFAULT + DEFAULT, } @Composable @@ -36,8 +34,8 @@ fun Tag( TagType.WARNING -> SurfaceColor.WarningContainer TagType.DEFAULT -> SurfaceColor.PrimaryContainer }, - shape = Shape.ExtraSmall - ).padding(horizontal = Spacing.Spacing8) + shape = Shape.ExtraSmall, + ).padding(horizontal = Spacing.Spacing8), ) { Text( modifier = Modifier.wrapContentSize(), @@ -50,4 +48,4 @@ fun Tag( }, ) } -} \ No newline at end of file +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/Modifiers.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/Modifiers.kt new file mode 100644 index 000000000..6c242ab95 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/Modifiers.kt @@ -0,0 +1,28 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.internal + +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +fun Modifier.bottomBorder(strokeWidth: Dp, color: Color) = composed( + factory = { + val density = LocalDensity.current + val strokeWidthPx = density.run { strokeWidth.toPx() } + + Modifier.drawBehind { + val width = size.width + val height = size.height - strokeWidthPx / 2 + + drawLine( + color = color, + start = Offset(x = 0f, y = height), + end = Offset(x = width, y = height), + strokeWidth = strokeWidthPx, + ) + } + }, +) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt index b51c4d61c..8e0a87f6e 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt @@ -4,3 +4,6 @@ import androidx.compose.runtime.Composable @Composable expect fun provideStringResource(id: String): String + +@Composable +expect fun provideQuantityStringResource(id: String, quantity: Int): String diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Ripple.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Ripple.kt index 5a2bb3616..ea71c34b3 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Ripple.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Ripple.kt @@ -8,13 +8,17 @@ import androidx.compose.ui.graphics.Color object Ripple { internal object CustomDHISRippleTheme : RippleTheme { + private val alpha = RippleAlpha( + focusedAlpha = 0.16f, + draggedAlpha = 0.16f, + hoveredAlpha = 0.16f, + pressedAlpha = 0.16f, + ) + @Composable override fun defaultColor(): Color = SurfaceColor.Primary @Composable - override fun rippleAlpha(): RippleAlpha = RippleTheme.defaultRippleAlpha( - SurfaceColor.Primary, - lightTheme = true, - ) + override fun rippleAlpha(): RippleAlpha = alpha } } diff --git a/designsystem/src/commonMain/resources/values/strings_en.xml b/designsystem/src/commonMain/resources/values/strings_en.xml index 837875caa..4f5375101 100644 --- a/designsystem/src/commonMain/resources/values/strings_en.xml +++ b/designsystem/src/commonMain/resources/values/strings_en.xml @@ -6,4 +6,11 @@ AGE OR Years + %d error + %d errors + %d warning + %d warnings + Show fields + Hide fields + Next \ No newline at end of file diff --git a/designsystem/src/desktopMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt b/designsystem/src/desktopMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt index 103c25943..8f67a931a 100644 --- a/designsystem/src/desktopMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt +++ b/designsystem/src/desktopMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/resource/String.kt @@ -2,6 +2,7 @@ package org.hisp.dhis.mobile.ui.designsystem.resource import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.res.useResource @@ -12,6 +13,15 @@ actual fun provideStringResource(id: String): String { return res[id] ?: "Key not found" } +@Composable +actual fun provideQuantityStringResource(id: String, quantity: Int): String { + val appendToId = when (quantity) { + 1 -> "one" + else -> "other" + } + return provideStringResource("${id}_$appendToId").format(quantity) +} + private fun getResources(): Map { val stringsResources = mutableMapOf() // for translation we could use Locale.current.language to find the proper xml From 041e81e827d70b6ad5fe14001099130140c05e29 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 7 Sep 2023 11:50:32 +0200 Subject: [PATCH 3/3] tests --- .../ui/designsystem/component/Sections.kt | 41 ++++- .../ui/designsystem/component/SectionTest.kt | 146 ++++++++++++++++++ 2 files changed, 183 insertions(+), 4 deletions(-) create mode 100644 designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SectionTest.kt diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt index 4cc82d739..b9c01ddfe 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Sections.kt @@ -36,8 +36,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.SemanticsPropertyReceiver +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.SectionSemantics.stateSemantic import org.hisp.dhis.mobile.ui.designsystem.component.internal.bottomBorder import org.hisp.dhis.mobile.ui.designsystem.resource.provideQuantityStringResource import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource @@ -113,7 +118,9 @@ fun Section( exit = shrinkVertically(shrinkTowards = Alignment.CenterVertically), ) { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .testTag(SectionTestTag.CONTENT) + .fillMaxWidth(), verticalArrangement = spacedBy(Spacing16), ) { content() @@ -174,6 +181,10 @@ fun SectionHeader( Column( modifier = modifier + .testTag(SectionTestTag.HEADER) + .semantics { + stateSemantic = sectionState + } .fillMaxWidth() .background(color = Color.White, Shape.Small) .clip(Shape.Small) @@ -196,12 +207,14 @@ fun SectionHeader( modifier = Modifier.weight(1f), ) { SectionTitle( - modifier.fillMaxWidth(), + modifier.fillMaxWidth() + .testTag(SectionTestTag.TITLE), title = title, ) description?.let { SupportingText( - modifier = Modifier.padding(Spacing.Spacing0), + modifier = Modifier.padding(Spacing.Spacing0) + .testTag(SectionTestTag.DESCRIPTION), text = it, maxLines = 2, onNoInteraction = { @@ -212,6 +225,7 @@ fun SectionHeader( } CompletionLabel( + modifier = Modifier.testTag(SectionTestTag.FIELD_PROGRESS), completedFields = completedFields, totalFields = totalFields, ) @@ -231,12 +245,14 @@ fun SectionHeader( } if (errorCount > 0) { Tag( + modifier = Modifier.testTag(SectionTestTag.ERROR_LABEL), label = provideQuantityStringResource("error", errorCount), type = TagType.ERROR, ) } if (warningCount > 0) { Tag( + modifier = Modifier.testTag(SectionTestTag.WARNING_LABEL), label = provideQuantityStringResource("warning", warningCount), type = TagType.WARNING, ) @@ -265,7 +281,7 @@ internal fun CompletionLabel( totalFields: Int, ) { Box( - modifier = modifier.padding( + modifier = Modifier.padding( start = Spacing.Spacing8, top = Spacing.Spacing4, bottom = Spacing.Spacing4, @@ -284,6 +300,7 @@ internal fun CompletionLabel( internal fun StateIndicator(label: String, icon: ImageVector?) { Row( modifier = Modifier + .testTag(SectionTestTag.STATE_LABEL) .requiredHeight(20.dp) .padding(end = Spacing.Spacing4), horizontalArrangement = spacedBy(Spacing.Spacing8), @@ -298,3 +315,19 @@ internal fun StateIndicator(label: String, icon: ImageVector?) { ) } } + +internal object SectionTestTag { + const val CONTENT = "CONTENT" + const val HEADER = "HEADER" + const val TITLE = "TITLE" + const val DESCRIPTION = "DESCRIPTION" + const val FIELD_PROGRESS = "FIELD_PROGRESS" + const val STATE_LABEL = "STATE_LABEL" + const val ERROR_LABEL = "ERROR_LABEL" + const val WARNING_LABEL = "WARNING_LABEL" +} + +internal object SectionSemantics { + val State = SemanticsPropertyKey("STATE") + var SemanticsPropertyReceiver.stateSemantic by State +} diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SectionTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SectionTest.kt new file mode 100644 index 000000000..853bbfb72 --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/SectionTest.kt @@ -0,0 +1,146 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import org.junit.Rule +import org.junit.Test + +class SectionTest { + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplaySectionInfoIfCollapsable() { + rule.setContent { + SectionHeader( + title = "Section title", + description = "description", + completedFields = 0, + totalFields = 1, + sectionState = SectionState.CLOSE, + errorCount = 1, + warningCount = 1, + onSectionClick = { + }, + ) + } + rule.onNodeWithTag(SectionTestTag.TITLE, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.DESCRIPTION, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.FIELD_PROGRESS, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.STATE_LABEL, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.ERROR_LABEL, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.WARNING_LABEL, useUnmergedTree = true).assertIsDisplayed() + } + + @Test + fun shouldNotDisplaySectionInfoIfCollapsable() { + rule.setContent { + SectionHeader( + title = "Section title", + description = null, + completedFields = 0, + totalFields = 1, + sectionState = SectionState.CLOSE, + errorCount = 0, + warningCount = 0, + onSectionClick = { + }, + ) + } + rule.onNodeWithTag(SectionTestTag.TITLE, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.DESCRIPTION, useUnmergedTree = true).assertDoesNotExist() + rule.onNodeWithTag(SectionTestTag.FIELD_PROGRESS, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.STATE_LABEL, useUnmergedTree = true).assertIsDisplayed() + rule.onNodeWithTag(SectionTestTag.ERROR_LABEL, useUnmergedTree = true).assertDoesNotExist() + rule.onNodeWithTag(SectionTestTag.WARNING_LABEL, useUnmergedTree = true).assertDoesNotExist() + } + + @Test + fun shouldNotDisplaySectionStateInfoIfFixed() { + rule.setContent { + SectionHeader( + title = "Section title", + description = null, + completedFields = 0, + totalFields = 1, + sectionState = SectionState.FIXED, + errorCount = 0, + warningCount = 0, + onSectionClick = { + }, + ) + } + rule.onNodeWithTag(SectionTestTag.STATE_LABEL, useUnmergedTree = true).assertDoesNotExist() + } + + @Test + fun shouldChangeSectionStateAndShowContent() { + rule.setContent { + Section( + title = "Section title", + description = null, + completedFields = 0, + totalFields = 1, + state = SectionState.CLOSE, + errorCount = 0, + warningCount = 0, + onNextSection = { + }, + content = { + }, + ) + } + rule.onNode( + hasTestTag(SectionTestTag.HEADER) + .and(SemanticsMatcher.expectValue(SectionSemantics.State, SectionState.CLOSE)), + ).assertExists() + rule.onNodeWithTag(SectionTestTag.CONTENT, useUnmergedTree = true).assertDoesNotExist() + rule.onNodeWithTag(SectionTestTag.HEADER).performClick() + rule.onNode( + hasTestTag(SectionTestTag.HEADER) + .and(SemanticsMatcher.expectValue(SectionSemantics.State, SectionState.OPEN)), + ).assertExists() + rule.onNodeWithTag(SectionTestTag.CONTENT, useUnmergedTree = true).assertExists() + rule.onNodeWithTag(SectionTestTag.HEADER).performClick() + rule.onNode( + hasTestTag(SectionTestTag.HEADER) + .and(SemanticsMatcher.expectValue(SectionSemantics.State, SectionState.CLOSE)), + ).assertExists() + rule.onNodeWithTag(SectionTestTag.CONTENT, useUnmergedTree = true).assertDoesNotExist() + } + + @Test + fun shouldNotChangeStateIfFixed() { + rule.setContent { + Section( + title = "Section title", + description = null, + completedFields = 0, + totalFields = 1, + state = SectionState.FIXED, + errorCount = 0, + warningCount = 0, + onNextSection = { + }, + content = { + }, + ) + } + + rule.onNode( + hasTestTag(SectionTestTag.HEADER) + .and(SemanticsMatcher.expectValue(SectionSemantics.State, SectionState.FIXED)), + ).assertExists() + rule.onNodeWithTag(SectionTestTag.CONTENT, useUnmergedTree = true).assertExists() + rule.onNodeWithTag(SectionTestTag.HEADER).performClick() + rule.onNode( + hasTestTag(SectionTestTag.HEADER) + .and(SemanticsMatcher.expectValue(SectionSemantics.State, SectionState.FIXED)), + ).assertExists() + rule.onNodeWithTag(SectionTestTag.CONTENT, useUnmergedTree = true).assertExists() + } +}