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 675df1e38..1cd7df213 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -47,6 +47,7 @@ import org.hisp.dhis.common.screens.InputPercentageScreen import org.hisp.dhis.common.screens.InputPhoneNumberScreen import org.hisp.dhis.common.screens.InputPositiveIntegerOrZeroScreen import org.hisp.dhis.common.screens.InputPositiveIntegerScreen +import org.hisp.dhis.common.screens.InputQRCodeScreen import org.hisp.dhis.common.screens.InputRadioButtonScreen import org.hisp.dhis.common.screens.InputScreen import org.hisp.dhis.common.screens.InputSequentialScreen @@ -59,7 +60,6 @@ import org.hisp.dhis.common.screens.LegendScreen import org.hisp.dhis.common.screens.ListCardScreen import org.hisp.dhis.common.screens.MetadataAvatarScreen import org.hisp.dhis.common.screens.ProgressScreen -import org.hisp.dhis.common.screens.QrCodeBlockScreen import org.hisp.dhis.common.screens.RadioButtonScreen import org.hisp.dhis.common.screens.SectionScreen import org.hisp.dhis.common.screens.SupportingTextScreen @@ -157,7 +157,7 @@ fun Main() { Components.INPUT_RADIO_BUTTON -> InputRadioButtonScreen() Components.INPUT_MATRIX -> InputMatrixScreen() Components.INPUT_SEQUENTIAL -> InputSequentialScreen() - Components.QR_CODE_BLOCK -> QrCodeBlockScreen() + Components.INPUT_QR_CODE -> InputQRCodeScreen() Components.INPUT_CHECK_BOX -> InputCheckBoxScreen() Components.BARCODE_BLOCK -> BarcodeBlockScreen() Components.INPUT_YES_ONLY_SWITCH -> InputYesOnlySwitchScreen() 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 4f62e6f8e..885742492 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 @@ -33,7 +33,7 @@ enum class Components(val label: String) { SWITCH("Switch"), INPUT_MATRIX("Input Matrix"), INPUT_SEQUENTIAL("Input Sequential"), - QR_CODE_BLOCK("QR Code Block"), + INPUT_QR_CODE("Input QR code"), INPUT_CHECK_BOX("Input Check Box"), BARCODE_BLOCK("Barcode Block"), AGE_FIELD("Age Field"), diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputQRCodeScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputQRCodeScreen.kt new file mode 100644 index 000000000..2a73010a3 --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/InputQRCodeScreen.kt @@ -0,0 +1,204 @@ +package org.hisp.dhis.common.screens + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FileDownload +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.QrCodeScanner +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.Description +import org.hisp.dhis.mobile.ui.designsystem.component.InputQRCode +import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState +import org.hisp.dhis.mobile.ui.designsystem.component.QrCodeBlock +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.resource.provideStringResource +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 + +@Composable +fun InputQRCodeScreen() { + ColumnComponentContainer { + var inputValue1 by rememberSaveable { mutableStateOf("889026a1-d01e-4d34-8209-81e8ed5c614b") } + var showEnabledQRBottomSheet by rememberSaveable { mutableStateOf(false) } + + Description("Default Input QR code", textColor = TextColor.OnSurfaceVariant) + InputQRCode( + "label", + state = InputShellState.UNFOCUSED, + onQRButtonClicked = { + showEnabledQRBottomSheet = !showEnabledQRBottomSheet + }, + inputText = inputValue1, + onValueChanged = { + if (it != null) { + inputValue1 = it + } + }, + + ) + + if (showEnabledQRBottomSheet) { + BottomSheetShell( + modifier = Modifier.testTag("LEGEND_BOTTOM_SHEET"), + title = provideStringResource("qr_code"), + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Button", + tint = SurfaceColor.Primary, + ) + }, + content = { + Row(horizontalArrangement = Arrangement.Center) { + QrCodeBlock(data = inputValue1) + } + }, + buttonBlock = { + Row( + Modifier + .fillMaxSize() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.Center, + ) { + Button( + onClick = {}, + modifier = Modifier + .padding(top = Spacing.Spacing4) + .width(Spacing.Spacing80) + .weight(1f), + shape = Shape.Full, + enabled = true, + colors = ButtonDefaults.buttonColors(Color.Transparent, TextColor.OnSurfaceVariant, Color.Transparent, TextColor.OnDisabledSurface), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Outlined.Share, + contentDescription = "Carousel Button", + ) + + Spacer(Modifier.size(Spacing.Spacing8)) + Text("Share", style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + Button( + onClick = {}, + modifier = Modifier + .padding(top = Spacing.Spacing4) + .width(Spacing.Spacing80) + .weight(1f), + shape = Shape.Full, + enabled = true, + colors = ButtonDefaults.buttonColors(Color.Transparent, TextColor.OnSurfaceVariant, Color.Transparent, TextColor.OnDisabledSurface), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Outlined.QrCodeScanner, + contentDescription = "Carousel Button", + ) + + Spacer(Modifier.size(Spacing.Spacing8)) + Text("Scan", style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + + Button( + onClick = {}, + modifier = Modifier + .padding(top = Spacing.Spacing4) + .width(Spacing.Spacing80) + .weight(1f), + shape = Shape.Full, + enabled = true, + colors = ButtonDefaults.buttonColors(Color.Transparent, TextColor.OnSurfaceVariant, Color.Transparent, TextColor.OnDisabledSurface), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = Icons.Outlined.FileDownload, + contentDescription = "Carousel Button", + ) + + Spacer(Modifier.size(Spacing.Spacing8)) + Text("Download", style = MaterialTheme.typography.labelSmall, textAlign = TextAlign.Center, maxLines = 2, overflow = TextOverflow.Ellipsis) + } + } + } + }, + ) { + showEnabledQRBottomSheet = false + } + } + + var inputValue2 by rememberSaveable { mutableStateOf("") } + Description("Required field Input QR code", textColor = TextColor.OnSurfaceVariant) + InputQRCode( + "label", + state = InputShellState.ERROR, + onQRButtonClicked = { + }, + inputText = inputValue2, + onValueChanged = { + if (it != null) { + inputValue2 = it + } + }, + isRequiredField = true, + supportingText = listOf(SupportingTextData("Required", SupportingTextState.ERROR)), + ) + Spacer(Modifier.size(Spacing.Spacing18)) + + Spacer(Modifier.size(Spacing.Spacing18)) + var inputValue by rememberSaveable { mutableStateOf("") } + Description("Disabled Input QR code", textColor = TextColor.OnSurfaceVariant) + InputQRCode( + "label", + state = InputShellState.DISABLED, + onQRButtonClicked = { + }, + inputText = inputValue, + onValueChanged = { + if (it != null) { + inputValue = it + } + }, + ) + } +} diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/QrCodeBlockScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/QrCodeBlockScreen.kt deleted file mode 100644 index 68a0547bb..000000000 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/QrCodeBlockScreen.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.hisp.dhis.common.screens - -import androidx.compose.material3.Divider -import androidx.compose.runtime.Composable -import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer -import org.hisp.dhis.mobile.ui.designsystem.component.QrCodeBlock -import org.hisp.dhis.mobile.ui.designsystem.component.RowComponentContainer - -@Composable -fun QrCodeBlockScreen() { - ColumnComponentContainer { - QrCodeBlock(data = "QR code value") - Divider() - QrCodeBlock(data = "889026a1-d01e-4d34-8209-81e8ed5c614b") - Divider() - QrCodeBlock(data = "l;kw1jheoi1u23iop1") - Divider() - RowComponentContainer { - QrCodeBlock(data = "563ce8df-8e0b-420c-a63c-fe000b1d1f11") - QrCodeBlock(data = "378c472d-bb05-4174-9fe5-f6dbf8f5de36") - } - } -} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt index e8e63ca6e..677ec3a0f 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt @@ -166,6 +166,7 @@ fun BottomSheetShell( modifier = Modifier .verticalScroll(rememberScrollState()) .fillMaxHeight(1f), + horizontalAlignment = Alignment.CenterHorizontally, ) { content?.let { it.invoke() diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputQRCode.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputQRCode.kt new file mode 100644 index 000000000..3edd753e8 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputQRCode.kt @@ -0,0 +1,71 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.QrCode2 +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.input.ImeAction + +/** + * DHIS2 Input QR Code. Wraps DHIS ยท [BasicTextInput]. + * @param title controls the text to be shown for the title + * @param state Manages the InputShell state + * @param onQRButtonClicked gives access to the action button event + * @param supportingText is a list of SupportingTextData that + * manages all the messages to be shown + * @param legendData manages the legendComponent + * @param inputText manages the value of the text in the input field + * @param isRequiredField controls whether the field is mandatory or not + * @param onNextClicked gives access to the imeAction event + * @param onValueChanged gives access to the onValueChanged event + * @param onFocusChanged gives access to the onFocusChanged returns true if + * item is focused + * @param imeAction controls the imeAction button to be shown + * @param modifier allows a modifier to be passed externally + */ +@Composable +fun InputQRCode( + title: String, + state: InputShellState = InputShellState.UNFOCUSED, + onQRButtonClicked: () -> Unit, + supportingText: List? = null, + legendData: LegendData? = null, + inputText: String? = null, + isRequiredField: Boolean = false, + onNextClicked: (() -> Unit)? = null, + onValueChanged: ((String?) -> Unit)? = null, + onFocusChanged: ((Boolean) -> Unit)? = null, + imeAction: ImeAction = ImeAction.Next, + modifier: Modifier = Modifier, +) { + BasicTextInput( + title = title, + state = state, + supportingText = supportingText, + legendData = legendData, + inputText = inputText, + isRequiredField = isRequiredField, + onNextClicked = onNextClicked, + onValueChanged = onValueChanged, + keyboardOptions = KeyboardOptions(imeAction = imeAction), + modifier = modifier, + testTag = "QR_CODE", + onFocusChanged = onFocusChanged, + actionButton = { + SquareIconButton( + modifier = Modifier.testTag("INPUT_QR_CODE_BUTTON"), + enabled = true, + icon = { + Icon( + imageVector = Icons.Outlined.QrCode2, + contentDescription = null, + ) + }, + onClick = onQRButtonClicked, + ) + }, + ) +} diff --git a/designsystem/src/commonMain/resources/values/strings_en.xml b/designsystem/src/commonMain/resources/values/strings_en.xml index 46dc66d0f..0075faf79 100644 --- a/designsystem/src/commonMain/resources/values/strings_en.xml +++ b/designsystem/src/commonMain/resources/values/strings_en.xml @@ -18,4 +18,5 @@ Yes No Enter phone number + QR code diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputQRCodeTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputQRCodeTest.kt new file mode 100644 index 000000000..9d0c655d2 --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputQRCodeTest.kt @@ -0,0 +1,88 @@ +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.test.assertIsEnabled +import androidx.compose.ui.test.assertTextEquals +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 InputQRCodeTest { + + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplayComponentCorrectly() { + rule.setContent { + var inputValue by remember { mutableStateOf("") } + + InputQRCode( + title = "Phone Number", + inputText = inputValue, + onValueChanged = { + if (it != null) { + inputValue = it + } + }, + onQRButtonClicked = { + // no-op + }, + ) + } + rule.onNodeWithTag("INPUT_QR_CODE").assertExists() + } + + @Test + fun shouldDeleteContentWhenResetIsClicked() { + rule.setContent { + var inputValue by remember { mutableStateOf("12345") } + + InputQRCode( + title = "Phone Number", + inputText = inputValue, + onValueChanged = { + if (it != null) { + inputValue = it + } + }, + onQRButtonClicked = { + // no-op + }, + ) + } + rule.onNodeWithTag("INPUT_QR_CODE").assertExists() + rule.onNodeWithTag("INPUT_QR_CODE_RESET_BUTTON").assertExists() + rule.onNodeWithTag("INPUT_QR_CODE_RESET_BUTTON").performClick() + rule.onNodeWithTag("INPUT_QR_CODE_FIELD").assertExists() + rule.onNodeWithTag("INPUT_QR_CODE_FIELD").assertTextEquals("") + } + + @Test + fun shouldShowActionButtonCorrectlyAndBeClickable() { + rule.setContent { + var inputValue by remember { mutableStateOf("12345") } + + InputQRCode( + title = "Phone Number", + inputText = inputValue, + onValueChanged = { + if (it != null) { + inputValue = it + } + }, + onQRButtonClicked = { + // no-op + }, + ) + } + rule.onNodeWithTag("INPUT_QR_CODE").assertExists() + rule.onNodeWithTag("INPUT_QR_CODE_BUTTON").assertExists() + rule.onNodeWithTag("INPUT_QR_CODE_BUTTON").assertIsEnabled() + } +}