Skip to content

Commit

Permalink
ANDROAPP-5573-mobile-ui-Create-InputDate-component (#105)
Browse files Browse the repository at this point in the history
* Add support for passing visual transformation in `BasicTextField`

* Rename `DateOfBirthTransformation` to `DateTransformation`

* Move `DATE_MASK` inside `DateTransformation`

* Rename `DATE_OF_BIRTH` regex to `DATE_TIME`

fixup! a73dafd80fb5810cadea87a3893b652f4338165c

* Add time transformation

* Make date and time transformation classes public

* Add a common interface for date time visual transformations

* Add date time transformation

* Add input date time component

* Rename `ActionIconType` to `DateTimeActionIconType`

* Update date time transformation mask

* Run code format

* Add legend data as param to `InputDateTime`

* Display input reset button if the input value is not null or blank

* Fix broken test

* Run code formatting
  • Loading branch information
msasikanth authored Oct 16, 2023
1 parent 15c2833 commit 671506d
Show file tree
Hide file tree
Showing 11 changed files with 550 additions and 26 deletions.
2 changes: 2 additions & 0 deletions common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.hisp.dhis.common.screens.ImageBlockScreen
import org.hisp.dhis.common.screens.InputAgeScreen
import org.hisp.dhis.common.screens.InputBarCodeScreen
import org.hisp.dhis.common.screens.InputCheckBoxScreen
import org.hisp.dhis.common.screens.InputDateTimeScreen
import org.hisp.dhis.common.screens.InputDropDownScreen
import org.hisp.dhis.common.screens.InputEmailScreen
import org.hisp.dhis.common.screens.InputIntegerScreen
Expand Down Expand Up @@ -179,6 +180,7 @@ fun Main() {
Components.INPUT_ORG_UNIT -> InputOrgUnitScreen()
Components.IMAGE_BLOCK -> ImageBlockScreen()
Components.INPUT_DROPDOWN -> InputDropDownScreen()
Components.INPUT_DATE_TIME -> InputDateTimeScreen()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ enum class Components(val label: String) {
INPUT_ORG_UNIT("Input Org. Unit"),
IMAGE_BLOCK("Image Block"),
INPUT_DROPDOWN("Input Dropdown"),
INPUT_DATE_TIME("Input Date Time"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package org.hisp.dhis.common.screens

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 org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer
import org.hisp.dhis.mobile.ui.designsystem.component.DateTimeActionIconType
import org.hisp.dhis.mobile.ui.designsystem.component.InputDateTime
import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState
import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTimeTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.TimeTransformation

@Composable
fun InputDateTimeScreen() {
ColumnComponentContainer {
var date by remember { mutableStateOf("") }
var time by remember { mutableStateOf("") }
var dateTime by remember { mutableStateOf("") }

InputDateTime(
title = "Label",
value = date,
visualTransformation = DateTransformation(),
actionIconType = DateTimeActionIconType.DATE,
onActionClicked = {
// no-op
},
onValueChanged = { date = it },
)

InputDateTime(
title = "Label",
value = time,
visualTransformation = TimeTransformation(),
actionIconType = DateTimeActionIconType.TIME,
onActionClicked = {
// no-op
},
onValueChanged = { time = it },
)

InputDateTime(
title = "Label",
value = dateTime,
visualTransformation = DateTimeTransformation(),
actionIconType = DateTimeActionIconType.DATE_TIME,
onActionClicked = {
// no-op
},
onValueChanged = { dateTime = it },
)

InputDateTime(
title = "Label",
value = "",
state = InputShellState.DISABLED,
onActionClicked = {
// no-op
},
onValueChanged = {
// no-op
},
)

InputDateTime(
title = "Label",
value = "",
isRequired = true,
state = InputShellState.ERROR,
onActionClicked = {
// no-op
},
onValueChanged = {
// no-op
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@ import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType.Age
import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType.DateOfBirth
import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType.None
import org.hisp.dhis.mobile.ui.designsystem.component.TimeUnitValues.YEARS
import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation.Companion.DATE_MASK
import org.hisp.dhis.mobile.ui.designsystem.component.internal.RegExValidations
import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource
import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing

// Update [DateOfBirthTransformation] when updating the mask
// Check the usages before modifying
private const val DATE_OF_BIRTH_MASK = "DDMMYYYY"

/**
* Input filed to enter date-of-birth or age
*
Expand Down Expand Up @@ -57,12 +54,11 @@ fun InputAge(
onValueChanged: (AgeInputType) -> Unit,
) {
val maxAgeCharLimit = 3
val allowedCharacters = RegExValidations.DATE_OF_BIRTH.regex
val allowedCharacters = RegExValidations.DATE_TIME.regex

val helperText = remember(inputType) {
when (inputType) {
None -> null
is DateOfBirth -> DATE_OF_BIRTH_MASK
None, is DateOfBirth -> null
is Age -> inputType.unit.value
}
}
Expand Down Expand Up @@ -202,7 +198,7 @@ private fun transformInputText(inputType: AgeInputType): String {
}

private fun updateDateOfBirth(inputType: DateOfBirth, newText: String): AgeInputType {
return if (newText.length <= DATE_OF_BIRTH_MASK.length) {
return if (newText.length <= DATE_MASK.length) {
inputType.copy(value = newText)
} else {
inputType
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.hisp.dhis.mobile.ui.designsystem.component

import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material.icons.outlined.Cancel
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
import androidx.compose.ui.text.input.KeyboardType
import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTimeVisualTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.RegExValidations
import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing

/**
* Input field to enter date, time or date&time. It will format content based on given visual
* transformation
*
* @param title: Label of the component.
* @param value: Input of the component in the format of DDMMYYYY/HHMM/DDMMYYYYHHMM
* @param actionIconType: Type of action icon to display. [DateTimeActionIconType.DATE_TIME], [DateTimeActionIconType.DATE], [DateTimeActionIconType.TIME]
* @param onActionClicked: Callback to handle the action when the calendar icon is clicked.
* @param state: [InputShellState]
* @param legendData: [LegendData]
* @param supportingText: List of [SupportingTextData] that manages all the messages to be shown.
* @param isRequired: Mark this input as marked
* @param visualTransformation: Pass a visual transformation to format the date input visually. By default uses [DateTransformation]
* @param onValueChanged: Callback to receive changes in the input in the format of DDMMYYYY/HHMM/DDMMYYYYHHMM
*/
@Composable
fun InputDateTime(
title: String,
value: String?,
actionIconType: DateTimeActionIconType = DateTimeActionIconType.DATE_TIME,
onActionClicked: () -> Unit,
modifier: Modifier = Modifier,
state: InputShellState = InputShellState.UNFOCUSED,
legendData: LegendData? = null,
supportingText: List<SupportingTextData>? = null,
isRequired: Boolean = false,
imeAction: ImeAction = ImeAction.Next,
visualTransformation: DateTimeVisualTransformation = DateTransformation(),
onFocusChanged: ((Boolean) -> Unit) = {},
onValueChanged: (String) -> Unit,
) {
val allowedCharacters = RegExValidations.DATE_TIME.regex

InputShell(
modifier = modifier.testTag("INPUT_DATE_TIME"),
title = title,
state = state,
isRequiredField = isRequired,
onFocusChanged = onFocusChanged,
inputField = {
BasicTextField(
modifier = Modifier
.testTag("INPUT_DATE_TIME_TEXT_FIELD")
.fillMaxWidth(),
inputText = value.orEmpty(),
isSingleLine = true,
onInputChanged = { newText ->
if (newText.length > visualTransformation.maskLength) {
return@BasicTextField
}

if (allowedCharacters.containsMatchIn(newText) || newText.isBlank()) {
onValueChanged.invoke(newText)
}
},
enabled = state != InputShellState.DISABLED,
state = state,
keyboardOptions = KeyboardOptions(imeAction = imeAction, keyboardType = KeyboardType.Number),
visualTransformation = visualTransformation,
)
},
primaryButton = {
if (!value.isNullOrBlank() && state != InputShellState.DISABLED) {
IconButton(
modifier = Modifier.testTag("INPUT_DATE_TIME_RESET_BUTTON").padding(Spacing.Spacing0),
icon = {
Icon(
imageVector = Icons.Outlined.Cancel,
contentDescription = "Icon Button",
)
},
onClick = {
onValueChanged.invoke("")
},
)
}
},
secondaryButton = {
val icon = when (actionIconType) {
DateTimeActionIconType.DATE, DateTimeActionIconType.DATE_TIME -> Icons.Filled.Event
DateTimeActionIconType.TIME -> Icons.Filled.Schedule
}

SquareIconButton(
modifier = Modifier.testTag("INPUT_DATE_TIME_ACTION_BUTTON")
.focusable(),
icon = {
Icon(
imageVector = icon,
contentDescription = null,
)
},
onClick = onActionClicked,
enabled = state != InputShellState.DISABLED,
)
},
supportingText = {
supportingText?.forEach { label ->
SupportingText(
label.text,
label.state,
)
}
},
legend = {
legendData?.let {
Legend(legendData, Modifier.testTag("INPUT_DATE_TIME_LEGEND"))
}
},
)
}

enum class DateTimeActionIconType {
DATE, TIME, DATE_TIME
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateOfBirthTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.DateTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.PrefixTransformation
import org.hisp.dhis.mobile.ui.designsystem.component.internal.SuffixTransformer
import org.hisp.dhis.mobile.ui.designsystem.theme.Color.Blue300
Expand Down Expand Up @@ -72,6 +72,9 @@ fun EmptyInput(
* @param modifier to pass a modifier if necessary
* @param state manages the color of cursor depending on the state of parent component
* @param keyboardOptions manages the ImeAction to be shown on the keyboard
* @param visualTransformation manages custom visual transformation. When null is passed it
* will use the visual transformation created based on helper style, when a visual transformation
* is passed it will ignore the helper style.
* @param onNextClicked gives access to the ImeAction event
*/
@OptIn(ExperimentalComposeUiApi::class)
Expand All @@ -86,27 +89,30 @@ fun BasicTextField(
modifier: Modifier = Modifier,
state: InputShellState = InputShellState.FOCUSED,
keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
visualTransformation: VisualTransformation? = null,
onNextClicked: (() -> Unit)? = null,
) {
val keyboardController = LocalSoftwareKeyboardController.current
var visualTransformation = VisualTransformation.None
var textFieldVisualTransformation = VisualTransformation.None

if (helperStyle != InputStyle.NONE) {
when (helperStyle) {
InputStyle.WITH_HELPER_BEFORE -> {
helper?.let { visualTransformation = PrefixTransformation(it, enabled) }
helper?.let { textFieldVisualTransformation = PrefixTransformation(it, enabled) }
}
InputStyle.WITH_DATE_OF_BIRTH_HELPER -> {
helper?.let { visualTransformation = DateOfBirthTransformation(it) }
textFieldVisualTransformation = DateTransformation()
}
else -> {
helper?.let {
visualTransformation = SuffixTransformer(it)
textFieldVisualTransformation = SuffixTransformer(it)
}
}
}
}

textFieldVisualTransformation = visualTransformation ?: textFieldVisualTransformation

val cursorColor by remember {
if (state == InputShellState.UNFOCUSED || state == InputShellState.FOCUSED) {
mutableStateOf(InputShellState.FOCUSED.color)
Expand Down Expand Up @@ -151,7 +157,7 @@ fun BasicTextField(
keyboardController?.hide()
},
),
visualTransformation = visualTransformation,
visualTransformation = textFieldVisualTransformation,
cursorBrush = SolidColor(cursorColor),
)
}
Expand Down
Loading

0 comments on commit 671506d

Please sign in to comment.