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 e48637529..194234b93 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -41,6 +41,7 @@ import org.hisp.dhis.common.screens.InputPositiveIntegerScreen import org.hisp.dhis.common.screens.InputRadioButtonScreen import org.hisp.dhis.common.screens.InputScreen import org.hisp.dhis.common.screens.InputTextScreen +import org.hisp.dhis.common.screens.InputYesNoFieldScreen import org.hisp.dhis.common.screens.LegendDescriptionScreen import org.hisp.dhis.common.screens.LegendScreen import org.hisp.dhis.common.screens.ProgressScreen @@ -133,6 +134,7 @@ fun Main() { Components.INPUT_NUMBER -> InputNumberScreen() Components.INPUT_LETTER -> InputLetterScreen() Components.INPUT_RADIO_BUTTON -> InputRadioButtonScreen() + Components.INPUT_YES_NO_FIELD -> InputYesNoFieldScreen() } } } 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 2a0f74e2e..9782ab5f1 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 @@ -27,4 +27,5 @@ enum class Components(val label: String) { BUTTON_BLOCK("Button block"), ICON_CARDS("Icon Cards"), INPUT_RADIO_BUTTON("Input Radio Button"), + INPUT_YES_NO_FIELD("Input Yes/No field"), } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesNoFieldScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesNoFieldScreen.kt new file mode 100644 index 000000000..5afdb21ca --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesNoFieldScreen.kt @@ -0,0 +1,67 @@ +package org.hisp.dhis.common.screens + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState +import org.hisp.dhis.mobile.ui.designsystem.component.InputYesNoField +import org.hisp.dhis.mobile.ui.designsystem.component.InputYesNoFieldValues +import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextData +import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextState +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing + +@Composable +fun InputYesNoFieldScreen() { + var selectedItem by remember { + mutableStateOf(null) + } + + var selectedItem1 by remember { + mutableStateOf(InputYesNoFieldValues.YES) + } + + var selectedItemError by remember { + mutableStateOf(null) + } + + ColumnComponentContainer("Yes/No Field") { + InputYesNoField( + title = "Label", + itemSelected = selectedItem, + onItemChange = { + selectedItem = it + }, + ) + Spacer(Modifier.size(Spacing.Spacing18)) + InputYesNoField( + title = "Label", + itemSelected = selectedItem1, + onItemChange = { + selectedItem1 = it + }, + ) + Spacer(Modifier.size(Spacing.Spacing18)) + InputYesNoField( + title = "Label", + state = InputShellState.ERROR, + supportingText = listOf(SupportingTextData("Error text", SupportingTextState.ERROR)), + itemSelected = selectedItemError, + onItemChange = { + selectedItemError = it + }, + ) + Spacer(Modifier.size(Spacing.Spacing18)) + InputYesNoField( + title = "Label", + state = InputShellState.DISABLED, + onItemChange = { + }, + ) + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesNoField.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesNoField.kt new file mode 100644 index 000000000..9a8a97d15 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesNoField.kt @@ -0,0 +1,100 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material3.Icon +import androidx.compose.material3.RadioButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource +import java.util.Locale + +/** + * DHIS2 Input Yes/No Field. Wraps DHIS ยท [RadioButton]. + * @param title controls the text to be shown for the title + * @param modifier allows a modifier to be passed externally + * @param orientation Controls how the radio buttons will be displayed, HORIZONTAL for rows or + * VERTICAL for columns. + * @param state Manages the InputShell state + * @param supportingText is a list of SupportingTextData that + * manages all the messages to be shown + * @param legendData manages the legendComponent + * @param isRequired controls whether the field is mandatory or not + * @param itemSelected controls which item is selected. + * @param onItemChange is a callback to notify which item has changed into the block. + */ +@Composable +fun InputYesNoField( + title: String, + modifier: Modifier = Modifier, + orientation: Orientation = Orientation.HORIZONTAL, + state: InputShellState = InputShellState.UNFOCUSED, + supportingText: List? = null, + legendData: LegendData? = null, + isRequired: Boolean = false, + itemSelected: InputYesNoFieldValues? = null, + onItemChange: (InputYesNoFieldValues?) -> Unit, +) { + InputShell( + modifier = modifier.testTag("INPUT_YES_NO_FIELD"), + isRequiredField = isRequired, + title = title, + state = state, + legend = { + legendData?.let { + Legend(legendData, modifier.testTag("INPUT_YES_NO_FIELD_LEGEND")) + } + }, + supportingText = { + supportingText?.forEach { label -> + SupportingText( + label.text, + label.state, + modifier = modifier.testTag("INPUT_YES_NO_FIELD_SUPPORTING_TEXT"), + ) + } + }, + inputField = { + val options = InputYesNoFieldValues.values().map { + RadioButtonData( + it.value, + itemSelected == it, + state != InputShellState.DISABLED, + provideStringResource(it.value.lowercase(Locale.getDefault())), + ) + } + RadioButtonBlock( + orientation, + options, + options.find { it.selected }, + ) { radioButtonData -> + onItemChange.invoke( + InputYesNoFieldValues.values().firstOrNull { it.name.equals(radioButtonData.uid, true) }, + ) + } + }, + primaryButton = { + val isClearButtonVisible = itemSelected != null && state != InputShellState.DISABLED + if (isClearButtonVisible) { + IconButton( + modifier = Modifier.testTag("INPUT_YES_NO_FIELD_CLEAR_BUTTON"), + icon = { + Icon( + imageVector = Icons.Outlined.Cancel, + contentDescription = "Icon Button", + ) + }, + onClick = { + onItemChange.invoke(null) + }, + ) + } + }, + ) +} + +enum class InputYesNoFieldValues(val value: String) { + YES("Yes"), + NO("No"), +} diff --git a/designsystem/src/commonMain/resources/values/strings_en.xml b/designsystem/src/commonMain/resources/values/strings_en.xml index 4f5375101..170b6300d 100644 --- a/designsystem/src/commonMain/resources/values/strings_en.xml +++ b/designsystem/src/commonMain/resources/values/strings_en.xml @@ -13,4 +13,6 @@ Show fields Hide fields Next + Yes + No \ No newline at end of file diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesNoFieldTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesNoFieldTest.kt new file mode 100644 index 000000000..3cf0641f2 --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesNoFieldTest.kt @@ -0,0 +1,187 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsNotSelected +import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.junit.Rule +import org.junit.Test + +class InputYesNoFieldTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplayInputYesNoFieldCorrectly() { + rule.setContent { + var selectedItem by remember { + mutableStateOf(null) + } + InputYesNoField( + title = "Label", + modifier = Modifier.testTag("INPUT_YES_NO_FIELD"), + itemSelected = selectedItem, + onItemChange = { + selectedItem = it + }, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_LEGEND").assertDoesNotExist() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_SUPPORTING_TEXT").assertDoesNotExist() + } + + @Test + fun shouldAllowUserSelectionWhenEnabled() { + rule.setContent { + var selectedItem by remember { + mutableStateOf(null) + } + InputYesNoField( + title = "Label", + modifier = Modifier.testTag("INPUT_YES_NO_FIELD"), + itemSelected = selectedItem, + onItemChange = { + selectedItem = it + }, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("RADIO_BUTTON_Yes").performClick() + rule.onNodeWithTag("RADIO_BUTTON_Yes").assertIsSelected() + } + + @Test + fun shouldNotAllowUserSelectionWhenDisabled() { + rule.setContent { + var selectedItem by remember { + mutableStateOf(null) + } + InputYesNoField( + title = "Label", + modifier = Modifier.testTag("INPUT_YES_NO_FIELD"), + state = InputShellState.DISABLED, + itemSelected = selectedItem, + onItemChange = { + selectedItem = it + }, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("RADIO_BUTTON_Yes").performClick() + rule.onNodeWithTag("RADIO_BUTTON_Yes").assertIsNotSelected() + } + + @Test + fun shouldShowClearButtonWhenItemSelected() { + rule.setContent { + var selectedItem by remember { + mutableStateOf(InputYesNoFieldValues.YES) + } + InputYesNoField( + title = "Label", + modifier = Modifier.testTag("INPUT_YES_NO_FIELD"), + itemSelected = selectedItem, + onItemChange = { + selectedItem = it + }, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_CLEAR_BUTTON").assertExists() + } + + @Test + fun shouldHideClearButtonWhenNoItemIsSelected() { + rule.setContent { + InputYesNoField( + title = "Label", + modifier = Modifier.testTag("INPUT_YES_NO_FIELD"), + onItemChange = { + }, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_CLEAR_BUTTON").assertDoesNotExist() + } + + @Test + fun shouldHideClearButtonWhenDisabled() { + rule.setContent { + var selectedItem by remember { + mutableStateOf(InputYesNoFieldValues.YES) + } + InputYesNoField( + title = "Label", + state = InputShellState.DISABLED, + itemSelected = selectedItem, + modifier = Modifier.testTag("INPUT_YES_NO_FIELD"), + onItemChange = { + selectedItem = it + }, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_CLEAR_BUTTON").assertDoesNotExist() + } + + @Test + fun shouldClearSelectionWhenClearButtonIsClickedAndHideClearButton() { + rule.setContent { + var selectedItem by remember { + mutableStateOf(InputYesNoFieldValues.YES) + } + InputYesNoField( + title = "Label", + modifier = Modifier.testTag("INPUT_YES_NO_FIELD"), + itemSelected = selectedItem, + onItemChange = { + selectedItem = it + }, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_CLEAR_BUTTON").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_CLEAR_BUTTON").performClick() + rule.onNodeWithTag("RADIO_BUTTON_Yes").assertIsNotSelected() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_CLEAR_BUTTON").assertDoesNotExist() + } + + @Test + fun shouldShowLegendCorrectly() { + rule.setContent { + InputYesNoField( + title = "Label", + legendData = LegendData(SurfaceColor.CustomGreen, "Legend"), + onItemChange = {}, + ) + } + + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_LEGEND").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_LEGEND").assertHasClickAction() + } + + @Test + fun shouldShowSupportingTextCorrectly() { + rule.setContent { + InputYesNoField( + title = "Label", + supportingText = listOf(SupportingTextData("Supporting text", SupportingTextState.DEFAULT)), + onItemChange = {}, + ) + } + rule.onNodeWithTag("INPUT_YES_NO_FIELD").assertExists() + rule.onNodeWithTag("INPUT_YES_NO_FIELD_SUPPORTING_TEXT").assertExists() + } +}