Skip to content

Commit

Permalink
Add InputAge component
Browse files Browse the repository at this point in the history
  • Loading branch information
msasikanth committed Oct 6, 2023
1 parent 61e1b58 commit 72304e9
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ 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.AgeInputType
import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer
import org.hisp.dhis.mobile.ui.designsystem.component.InputAge
import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState
import org.hisp.dhis.mobile.ui.designsystem.component.Orientation
import org.hisp.dhis.mobile.ui.designsystem.component.RadioButtonData
import org.hisp.dhis.mobile.ui.designsystem.component.SubTitle
Expand All @@ -14,13 +17,94 @@ import org.hisp.dhis.mobile.ui.designsystem.component.TimeUnitValues

@Composable
fun InputAgeScreen() {
ColumnComponentContainer("Age Field components") {
ColumnComponentContainer {
SubTitle("Horizontal Age Field Helper")
var selectedFieldHorizontal by remember {
mutableStateOf(RadioButtonData("0", selected = true, enabled = true, textInput = TimeUnitValues.YEARS.value))
}
TimeUnitSelector(Orientation.HORIZONTAL, TimeUnitValues.YEARS.value) {
selectedFieldHorizontal = it
}

SubTitle("Input Age Component - Idle")
var inputType by remember { mutableStateOf<AgeInputType>(AgeInputType.None) }

InputAge(
title = "Label",
inputType = inputType,
onCalendarActionClicked = {
// no-op
},
onValueChanged = { newInputType ->
inputType = newInputType
},
)

SubTitle("Input Age Component - Idle Disabled")
InputAge(
title = "Label",
inputType = AgeInputType.None,
state = InputShellState.DISABLED,
onCalendarActionClicked = {
// no-op
},
onValueChanged = { newInputType ->
inputType = newInputType
},
)

SubTitle("Input Age Component - Date Of Birth")
InputAge(
title = "Label",
inputType = AgeInputType.DateOfBirth("01011985"),
state = InputShellState.DISABLED,
onCalendarActionClicked = {
// no-op
},
onValueChanged = { newInputType ->
inputType = newInputType
},
)

SubTitle("Input Age Component - Date Of Birth Required Error")
InputAge(
title = "Label",
inputType = AgeInputType.DateOfBirth("010"),
state = InputShellState.ERROR,
isRequired = true,
onCalendarActionClicked = {
// no-op
},
onValueChanged = {
// no-op
},
)

SubTitle("Input Age Component - Age Disabled")
InputAge(
title = "Label",
inputType = AgeInputType.Age(value = "56", unit = TimeUnitValues.YEARS),
state = InputShellState.DISABLED,
onCalendarActionClicked = {
// no-op
},
onValueChanged = { newInputType ->
inputType = newInputType
},
)

SubTitle("Input Age Component - Age Required Error")
InputAge(
title = "Label",
inputType = AgeInputType.Age(value = "56", unit = TimeUnitValues.YEARS),
state = InputShellState.ERROR,
isRequired = true,
onCalendarActionClicked = {
// no-op
},
onValueChanged = {
// no-op
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,26 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor
fun TimeUnitSelector(
orientation: Orientation,
optionSelected: String,
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: (RadioButtonData) -> Unit,
) {
val backgroundColor = if (enabled) {
SurfaceColor.Surface
} else {
SurfaceColor.DisabledSurface
}

RowComponentContainer(
modifier = Modifier
.background(color = SurfaceColor.Surface, Shape.SmallBottom)
modifier = modifier
.background(color = backgroundColor, Shape.SmallBottom)
.padding(
start = Spacing.Spacing8,
end = Spacing.Spacing8,
),
) {
val options = TimeUnitValues.values().map {
RadioButtonData(it.value, optionSelected == it.value, true, provideStringResource(it.value))
RadioButtonData(it.value, optionSelected == it.value, enabled, provideStringResource(it.value))
}
val selectedItem = options.find {
it.selected
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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.material.icons.Icons
import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
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.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
*
* @param title: Label of the component.
* @param inputType: The type of input [None], [DateOfBirth], [Age].
* @param onCalendarActionClicked: Callback to handle the action when the calendar icon is clicked.
* @param state: [InputShellState]
* @param supportingText: List of [SupportingTextData] that manages all the messages to be shown.
* @param isRequired: Mark this input as marked
* @param onValueChanged: Callback to receive changes in the input
*/
@Composable
fun InputAge(
title: String,
inputType: AgeInputType = None,
onCalendarActionClicked: () -> Unit,
modifier: Modifier = Modifier,
state: InputShellState = InputShellState.UNFOCUSED,
supportingText: List<SupportingTextData>? = null,
isRequired: Boolean = false,
testTag: String = "",
dateOfBirthLabel: String = provideStringResource("date_birth"),
orLabel: String = provideStringResource("or"),
ageLabel: String = provideStringResource("age"),
onValueChanged: (AgeInputType) -> Unit,
) {
val maxAgeCharLimit = 3
val allowedCharacters = RegExValidations.DATE_OF_BIRTH.regex

val helperText = remember(inputType) {
when (inputType) {
None -> null
is DateOfBirth -> DATE_OF_BIRTH_MASK
is Age -> inputType.unit.value
}
}
val helperStyle = remember(inputType) {
when (inputType) {
None -> InputStyle.NONE
is DateOfBirth -> InputStyle.WITH_DATE_OF_BIRTH_HELPER
is Age -> InputStyle.WITH_HELPER_AFTER
}
}

val calendarButton: (@Composable () -> Unit)? = if (inputType is DateOfBirth) {
@Composable {
SquareIconButton(
modifier = Modifier.testTag("INPUT_AGE_${testTag}_OPEN_CALENDAR_BUTTON"),
icon = {
Icon(
imageVector = Icons.Filled.Event,
contentDescription = null,
)
},
onClick = onCalendarActionClicked,
enabled = state != InputShellState.DISABLED,
)
}
} else {
null
}

InputShell(
modifier = modifier.testTag("INPUT_AGE_$testTag"),
title = title,
state = state,
isRequiredField = isRequired,
inputField = {
when (inputType) {
None -> {
TextButtonSelector(
modifier = Modifier.focusable(true),
firstOptionText = dateOfBirthLabel,
onClickFirstOption = {
onValueChanged.invoke(DateOfBirth.EMPTY)
},
middleText = orLabel,
secondOptionText = ageLabel,
onClickSecondOption = {
onValueChanged.invoke(Age.EMPTY)
},
enabled = state != InputShellState.DISABLED,
)
}
is DateOfBirth, is Age -> {
BasicTextField(
modifier = Modifier
.testTag("INPUT_" + testTag + "_FIELD")
.fillMaxWidth(),
inputText = transformInputText(inputType),
helper = helperText,
isSingleLine = true,
helperStyle = helperStyle,
onInputChanged = { newText ->
if (newText.length > maxAgeCharLimit && inputType is Age) {
return@BasicTextField
}

@Suppress("KotlinConstantConditions")
val newInputType: AgeInputType = when (inputType) {
is Age -> inputType.copy(value = newText)
is DateOfBirth -> updateDateOfBirth(inputType, newText)
None -> None
}

if (allowedCharacters.containsMatchIn(newText) || newText.isBlank()) {
onValueChanged.invoke(newInputType)
}
},
enabled = state != InputShellState.DISABLED,
state = state,
)
}
}
},
primaryButton = {
if (inputType != None) {
IconButton(
modifier = Modifier.testTag("INPUT_AGE_" + testTag + "_RESET_BUTTON").padding(Spacing.Spacing0),
icon = {
Icon(
imageVector = Icons.Outlined.Cancel,
contentDescription = "Icon Button",
)
},
onClick = {
onValueChanged.invoke(None)
},
enabled = state != InputShellState.DISABLED,
)
}
},
secondaryButton = calendarButton,
supportingText = {
supportingText?.forEach { label ->
SupportingText(
label.text,
label.state,
modifier = Modifier.testTag("INPUT_AGE_" + testTag + "_SUPPORTING_TEXT"),
)
}
},
legend = {
if (inputType is Age) {
TimeUnitSelector(
modifier = Modifier.fillMaxWidth(),
orientation = Orientation.HORIZONTAL,
optionSelected = YEARS.value,
enabled = state != InputShellState.DISABLED,
onClick = { itemData ->
val timeUnit = TimeUnitValues.values()
.first { it.value.contains(itemData.textInput!!, ignoreCase = true) }

onValueChanged.invoke(inputType.copy(unit = timeUnit))
},
)
}
},
)
}

private fun transformInputText(inputType: AgeInputType): String {
return when (inputType) {
is Age -> inputType.value
is DateOfBirth -> inputType.value
None -> ""
}
}

private fun updateDateOfBirth(inputType: DateOfBirth, newText: String): AgeInputType {
return if (newText.length <= DATE_OF_BIRTH_MASK.replace("/", "").length) {
inputType.copy(value = newText)
} else {
inputType
}
}

sealed interface AgeInputType {
data object None : AgeInputType

data class DateOfBirth(val value: String) : AgeInputType {
companion object {
val EMPTY = DateOfBirth("")
}
}

data class Age(val value: String, val unit: TimeUnitValues) : AgeInputType {
companion object {
val EMPTY = Age("", YEARS)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +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.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 @@ -91,11 +92,17 @@ fun BasicTextField(
var visualTransformation = VisualTransformation.None

if (helperStyle != InputStyle.NONE) {
if (helperStyle == InputStyle.WITH_HELPER_BEFORE) {
helper?.let { visualTransformation = PrefixTransformation(it, enabled) }
} else {
helper?.let {
visualTransformation = SuffixTransformer(it)
when (helperStyle) {
InputStyle.WITH_HELPER_BEFORE -> {
helper?.let { visualTransformation = PrefixTransformation(it, enabled) }
}
InputStyle.WITH_DATE_OF_BIRTH_HELPER -> {
helper?.let { visualTransformation = DateOfBirthTransformation(it) }
}
else -> {
helper?.let {
visualTransformation = SuffixTransformer(it)
}
}
}
}
Expand Down Expand Up @@ -153,5 +160,6 @@ fun BasicTextField(
enum class InputStyle {
WITH_HELPER_AFTER,
WITH_HELPER_BEFORE,
WITH_DATE_OF_BIRTH_HELPER,
NONE,
}
Loading

0 comments on commit 72304e9

Please sign in to comment.