Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ANDROAPP-5542-mobile-ui-Create-InputAge-component #96

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading