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 10, 2023
1 parent b32d251 commit 299ec8c
Show file tree
Hide file tree
Showing 7 changed files with 576 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,226 @@
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.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 androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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] : default,
* [DateOfBirth] : In ddmmyyyy format,
* [Age]: Age value with appropriate time unit
* @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,
imeAction: ImeAction = ImeAction.Next,
dateOfBirthLabel: String = provideStringResource("date_birth"),
orLabel: String = provideStringResource("or"),
ageLabel: String = provideStringResource("age"),
onFocusChanged: ((Boolean) -> Unit) = {},
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_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"),
title = title,
state = state,
isRequiredField = isRequired,
onFocusChanged = onFocusChanged,
inputField = {
when (inputType) {
None -> {
TextButtonSelector(
modifier = Modifier.focusable(true)
.testTag("INPUT_AGE_MODE_SELECTOR"),
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_AGE_TEXT_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,
keyboardOptions = KeyboardOptions(imeAction = imeAction, keyboardType = KeyboardType.Number),
)
}
}
},
primaryButton = {
if (inputType != None && state != InputShellState.DISABLED) {
IconButton(
modifier = Modifier.testTag("INPUT_AGE_RESET_BUTTON").padding(Spacing.Spacing0),
icon = {
Icon(
imageVector = Icons.Outlined.Cancel,
contentDescription = "Icon Button",
)
},
onClick = {
onValueChanged.invoke(None)
},
)
}
},
secondaryButton = calendarButton,
supportingText = {
supportingText?.forEach { label ->
SupportingText(
label.text,
label.state,
)
}
},
legend = {
if (inputType is Age) {
TimeUnitSelector(
modifier = Modifier.fillMaxWidth()
.testTag("INPUT_AGE_TIME_UNIT_SELECTOR"),
orientation = Orientation.HORIZONTAL,
optionSelected = YEARS.value,
enabled = state != InputShellState.DISABLED,
onClick = { itemData ->
val timeUnit = TimeUnitValues.entries
.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.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)
}
}
}
Loading

0 comments on commit 299ec8c

Please sign in to comment.