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 d1f4bbdb5..551dff6dc 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -43,6 +43,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.InputYesOnlyCheckBoxScreen import org.hisp.dhis.common.screens.InputYesOnlySwitchScreen import org.hisp.dhis.common.screens.LegendDescriptionScreen import org.hisp.dhis.common.screens.LegendScreen @@ -141,6 +142,7 @@ fun Main() { Components.SWITCH -> SwitchScreen() Components.INPUT_RADIO_BUTTON -> InputRadioButtonScreen() Components.INPUT_YES_ONLY_SWITCH -> InputYesOnlySwitchScreen() + Components.INPUT_YES_ONLY_CHECKBOX -> InputYesOnlyCheckBoxScreen() } } } 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 b2fcefa58..a9eb4a2e1 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 @@ -31,4 +31,5 @@ enum class Components(val label: String) { INPUT_RADIO_BUTTON("Input Radio Button"), SWITCH("Switch"), INPUT_YES_ONLY_SWITCH("Input yes only switch"), + INPUT_YES_ONLY_CHECKBOX("Input yes only checkbox"), } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesOnlyCheckBoxScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesOnlyCheckBoxScreen.kt new file mode 100644 index 000000000..28d696806 --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesOnlyCheckBoxScreen.kt @@ -0,0 +1,69 @@ +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.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import org.hisp.dhis.mobile.ui.designsystem.component.CheckBoxData +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.InputYesOnlyCheckBox +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 InputYesOnlyCheckBoxScreen() { + ColumnComponentContainer { + var checkboxData by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = true, textInput = "Label")) + } + var checkboxData1 by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = true, enabled = true, textInput = "Label")) + } + var checkboxData2 by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = true, textInput = "Label")) + } + var checkboxData3 by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = true, textInput = "Label")) + } + var checkboxData4 by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = true, enabled = true, textInput = "Label")) + } + InputYesOnlyCheckBox( + checkBoxData = checkboxData, + ) { + checkboxData = checkboxData.copy(checked = !checkboxData.checked) + } + Spacer(Modifier.size(Spacing.Spacing18)) + InputYesOnlyCheckBox( + checkBoxData = checkboxData1, + ) { + checkboxData1 = checkboxData1.copy(checked = !checkboxData.checked) + } + Spacer(Modifier.size(Spacing.Spacing18)) + InputYesOnlyCheckBox( + checkBoxData = checkboxData2, + state = InputShellState.ERROR, + supportingText = listOf(SupportingTextData("Error text", SupportingTextState.ERROR)), + ) { + checkboxData2 = checkboxData2.copy(checked = !checkboxData.checked) + } + Spacer(Modifier.size(Spacing.Spacing18)) + InputYesOnlyCheckBox( + checkBoxData = checkboxData3, + state = InputShellState.DISABLED, + ) { + } + Spacer(Modifier.size(Spacing.Spacing18)) + InputYesOnlyCheckBox( + checkBoxData = checkboxData4, + state = InputShellState.DISABLED, + ) { + } + } +} diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesOnlySwitchScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesOnlySwitchScreen.kt index cfb23a199..d554234af 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesOnlySwitchScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputYesOnlySwitchScreen.kt @@ -11,6 +11,8 @@ 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.InputYesOnlySwitch +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 @@ -39,6 +41,7 @@ fun InputYesOnlySwitchScreen() { title = "Label", isChecked = isSelected2, state = InputShellState.ERROR, + supportingText = listOf(SupportingTextData("Error text", SupportingTextState.ERROR)), ) { isSelected2 = !isSelected2 } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/CheckBox.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/CheckBox.kt index 41ee40b6a..a7c9ebd49 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/CheckBox.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/CheckBox.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import org.hisp.dhis.mobile.ui.designsystem.theme.InternalSizeValues import org.hisp.dhis.mobile.ui.designsystem.theme.Outline import org.hisp.dhis.mobile.ui.designsystem.theme.Ripple @@ -81,7 +82,8 @@ fun CheckBox( interactionSource = interactionSource, enabled = checkBoxData.enabled, modifier = Modifier - .size(InternalSizeValues.Size40), + .size(InternalSizeValues.Size40) + .testTag("CHECK_BOX_${checkBoxData.uid}"), colors = CheckboxDefaults.colors( checkedColor = SurfaceColor.Primary, uncheckedColor = Outline.Dark, diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputShell.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputShell.kt index d86c69f5a..ed11369e2 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputShell.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputShell.kt @@ -70,7 +70,8 @@ fun InputShell( state = rememberScrollState(), ).heightIn(Spacing.Spacing0, InternalSizeValues.Size300) .onFocusChanged { - indicatorColor = if (it.isFocused && state != InputShellState.ERROR && state != InputShellState.WARNING) InputShellState.FOCUSED.color else state.color + indicatorColor = + if (it.isFocused && state != InputShellState.ERROR && state != InputShellState.WARNING) InputShellState.FOCUSED.color else state.color indicatorThickness = if (it.isFocused) Border.Regular else Border.Thin }, backgroundColor = backgroundColor, diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesOnlyCheckBox.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesOnlyCheckBox.kt new file mode 100644 index 000000000..933b20e11 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesOnlyCheckBox.kt @@ -0,0 +1,63 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.layout.padding +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 androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.testTag +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing + +@Composable +fun InputYesOnlyCheckBox( + checkBoxData: CheckBoxData, + modifier: Modifier = Modifier, + state: InputShellState = InputShellState.UNFOCUSED, + supportingText: List? = null, + legendData: LegendData? = null, + isRequired: Boolean = false, + onClick: (Boolean) -> Unit, +) { + var indicatorColor by remember { mutableStateOf(InputShellState.UNFOCUSED.color) } + InputShell( + modifier = modifier + .onFocusChanged { + indicatorColor = + if (it.isFocused && state != InputShellState.ERROR && state != InputShellState.WARNING) InputShellState.FOCUSED.color else state.color + } + .testTag("INPUT_YES_ONLY_CHECKBOX"), + isRequiredField = isRequired, + title = "", + state = state, + legend = { + legendData?.let { + Legend(legendData, modifier.testTag("INPUT_YES_ONLY_CHECKBOX_LEGEND")) + } + }, + supportingText = { + supportingText?.forEach { label -> + SupportingText( + label.text, + label.state, + modifier = modifier.testTag("INPUT_YES_ONLY_CHECKBOX_SUPPORTING_TEXT"), + ) + } + }, + inputField = { + CheckBox( + modifier = Modifier.padding(bottom = Spacing.Spacing4), + checkBoxData = CheckBoxData( + uid = checkBoxData.uid, + checked = checkBoxData.checked, + enabled = if (state == InputShellState.DISABLED) false else checkBoxData.enabled, + textInput = checkBoxData.textInput, + ), + ) { + onClick.invoke(it) + } + }, + ) +} diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesOnlyCheckBoxTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesOnlyCheckBoxTest.kt new file mode 100644 index 000000000..dbafe49f1 --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputYesOnlyCheckBoxTest.kt @@ -0,0 +1,122 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +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.platform.testTag +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.assertIsNotEnabled +import androidx.compose.ui.test.isFocused +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 InputYesOnlyCheckBoxTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplayInputYesOnlyCheckBoxCorrectly() { + rule.setContent { + var checkboxData by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = true, textInput = "Label")) + } + InputYesOnlyCheckBox( + checkBoxData = checkboxData, + modifier = Modifier.testTag("INPUT_YES_ONLY_CHECKBOX"), + onClick = { + }, + ) + } + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX").assertExists() + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX_LEGEND").assertDoesNotExist() + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX_SUPPORTING_TEXT").assertDoesNotExist() + } + + @Test + fun shouldAllowUserSelectionWhenEnabled() { + rule.setContent { + var checkboxData by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = true, textInput = "Label")) + } + InputYesOnlyCheckBox( + checkBoxData = checkboxData, + modifier = Modifier.testTag("INPUT_YES_ONLY_CHECKBOX"), + onClick = { + checkboxData = checkboxData.copy(checked = !checkboxData.checked) + }, + ) + } + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX").assertExists() + rule.onNodeWithTag("CHECK_BOX_0").performClick() + rule.onNodeWithTag("CHECK_BOX_0").assertIsFocused() + rule.onNodeWithTag("CHECK_BOX_0").assertIsEnabled() + } + + @Test + fun shouldNotAllowUserSelectionWhenDisabled() { + rule.setContent { + var checkboxData by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = false, textInput = "Label")) + } + InputYesOnlyCheckBox( + checkBoxData = checkboxData, + modifier = Modifier.testTag("INPUT_YES_ONLY_CHECKBOX"), + state = InputShellState.DISABLED, + onClick = { + checkboxData = checkboxData.copy(checked = !checkboxData.checked) + }, + ) + } + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX").assertExists() + rule.onNodeWithTag("CHECK_BOX_0").performClick() + rule.onNodeWithTag("CHECK_BOX_0").assertIsNotEnabled() + rule.onNodeWithTag("CHECK_BOX_0").assert(!isFocused()) + } + + @Test + fun shouldShowLegendCorrectly() { + rule.setContent { + var checkboxData by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = true, textInput = "Label")) + } + InputYesOnlyCheckBox( + checkBoxData = checkboxData, + legendData = LegendData(SurfaceColor.CustomGreen, "Legend"), + onClick = { + checkboxData = checkboxData.copy(enabled = !checkboxData.enabled) + }, + ) + } + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX").assertExists() + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX_LEGEND").assertExists() + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX_LEGEND").assertHasClickAction() + } + + @Test + fun shouldShowSupportingTextCorrectly() { + rule.setContent { + var checkboxData by rememberSaveable { + mutableStateOf(CheckBoxData(uid = "0", checked = false, enabled = true, textInput = "Label")) + } + InputYesOnlyCheckBox( + checkBoxData = checkboxData, + supportingText = listOf(SupportingTextData("Supporting text", SupportingTextState.DEFAULT)), + onClick = { + checkboxData = checkboxData.copy(enabled = !checkboxData.enabled) + }, + ) + } + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX").assertExists() + rule.onNodeWithTag("INPUT_YES_ONLY_CHECKBOX_SUPPORTING_TEXT").assertExists() + } +}