1616
1717package com.google.android.fhir.datacapture.views.compose
1818
19- import androidx.compose.foundation.text.KeyboardActions
2019import androidx.compose.foundation.text.KeyboardOptions
20+ import androidx.compose.foundation.text.input.InputTransformation
21+ import androidx.compose.foundation.text.input.OutputTransformation
22+ import androidx.compose.foundation.text.input.TextFieldLineLimits
23+ import androidx.compose.foundation.text.input.insert
24+ import androidx.compose.foundation.text.input.maxLength
25+ import androidx.compose.foundation.text.input.rememberTextFieldState
26+ import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
27+ import androidx.compose.foundation.text.input.then
2128import androidx.compose.material3.DatePicker
2229import androidx.compose.material3.DatePickerDialog
2330import androidx.compose.material3.ExperimentalMaterial3Api
@@ -35,10 +42,9 @@ import androidx.compose.runtime.getValue
3542import androidx.compose.runtime.mutableStateOf
3643import androidx.compose.runtime.remember
3744import androidx.compose.runtime.setValue
45+ import androidx.compose.runtime.snapshotFlow
3846import androidx.compose.ui.Modifier
39- import androidx.compose.ui.focus.FocusDirection
4047import androidx.compose.ui.focus.onFocusChanged
41- import androidx.compose.ui.platform.LocalFocusManager
4248import androidx.compose.ui.platform.LocalSoftwareKeyboardController
4349import androidx.compose.ui.platform.testTag
4450import androidx.compose.ui.res.painterResource
@@ -51,6 +57,7 @@ import com.google.android.fhir.datacapture.R
5157import com.google.android.fhir.datacapture.extensions.format
5258import com.google.android.fhir.datacapture.extensions.toLocalDate
5359import java.time.LocalDate
60+ import kotlinx.coroutines.flow.collectLatest
5461
5562@OptIn(ExperimentalMaterial3Api ::class )
5663@Composable
@@ -67,47 +74,65 @@ internal fun DatePickerItem(
6774 parseStringToLocalDate : (String , DateFormatPattern ) -> LocalDate ? ,
6875 onDateInputEntry : (DateInput ) -> Unit ,
6976) {
70- val focusManager = LocalFocusManager .current
7177 val keyboardController = LocalSoftwareKeyboardController .current
7278 var dateInputState by remember(dateInput) { mutableStateOf(dateInput) }
73- val dateInputDisplay by remember(dateInputState) { derivedStateOf { dateInputState.display } }
79+ val textFieldState = rememberTextFieldState(dateInputState.display)
80+ var isFocused by remember { mutableStateOf(false ) }
81+
82+ val firstDelimiterIndex =
83+ remember(dateInputFormat) {
84+ dateInputFormat.patternWithDelimiters.indexOf(dateInputFormat.delimiter)
85+ }
86+ val secondDelimiterIndex =
87+ remember(dateInputFormat) {
88+ dateInputFormat.patternWithDelimiters.lastIndexOf(dateInputFormat.delimiter)
89+ }
90+ val dateFormatLength =
91+ remember(dateInputFormat) { dateInputFormat.patternWithoutDelimiters.length }
7492
7593 var showDatePickerModal by remember { mutableStateOf(false ) }
7694
77- LaunchedEffect (dateInputState) {
78- if (dateInputState != dateInput) {
79- onDateInputEntry(dateInputState)
95+ // Sync external dateInput changes to textFieldState
96+ LaunchedEffect (dateInput) {
97+ if (! isFocused && dateInput.display != textFieldState.text.toString()) {
98+ textFieldState.setTextAndPlaceCursorAtEnd(dateInput.display)
8099 }
81100 }
82101
83- OutlinedTextField (
84- value = dateInputDisplay,
85- onValueChange = {
86- if (
87- it.length <= dateInputFormat.patternWithoutDelimiters.length &&
88- it.all { char -> char.isDigit() }
89- ) {
102+ // Monitor textFieldState changes and update dateInputState
103+ LaunchedEffect (textFieldState) {
104+ snapshotFlow { textFieldState.text.toString() }
105+ .collectLatest {
90106 val trimmedText = it.trim()
91107 val localDate =
92- if (
93- trimmedText.isNotBlank() &&
94- trimmedText.length == dateInputFormat.patternWithoutDelimiters.length
95- ) {
108+ if (trimmedText.isNotBlank() && trimmedText.length == dateFormatLength) {
96109 parseStringToLocalDate(trimmedText, dateInputFormat.patternWithoutDelimiters)
97110 } else {
98111 null
99112 }
100- dateInputState = DateInput (it, localDate)
113+ val newDateInput = DateInput (trimmedText, localDate)
114+ if (dateInputState != newDateInput) {
115+ dateInputState = newDateInput
116+ onDateInputEntry(newDateInput)
117+ }
101118 }
102- },
103- singleLine = true ,
119+ }
120+
121+ OutlinedTextField (
122+ state = textFieldState,
123+ lineLimits = TextFieldLineLimits .SingleLine ,
104124 label = { Text (labelText) },
105125 modifier =
106126 modifier
107127 .testTag(DATE_TEXT_INPUT_FIELD )
108128 .onFocusChanged {
129+ isFocused = it.isFocused
109130 if (! it.isFocused) {
110131 keyboardController?.hide()
132+ // Sync external dateInput changes to textFieldState
133+ if (dateInput.display != textFieldState.text.toString()) {
134+ textFieldState.setTextAndPlaceCursorAtEnd(dateInput.display)
135+ }
111136 }
112137 }
113138 .semantics { if (isError) error(helperText ? : " " ) },
@@ -122,17 +147,25 @@ internal fun DatePickerItem(
122147 }
123148 },
124149 enabled = enabled,
150+ inputTransformation =
151+ InputTransformation .maxLength(dateFormatLength).then {
152+ if (asCharSequence().any { ! Character .isDigit(it) }) revertAllChanges()
153+ },
125154 keyboardOptions =
126155 KeyboardOptions (
127156 autoCorrectEnabled = false ,
128157 keyboardType = KeyboardType .Number ,
129158 imeAction = ImeAction .Done ,
130159 ),
131- keyboardActions =
132- KeyboardActions (
133- onNext = { focusManager.moveFocus(FocusDirection .Down ) },
134- ),
135- visualTransformation = DateVisualTransformation (dateInputFormat),
160+ outputTransformation =
161+ OutputTransformation {
162+ if (length >= firstDelimiterIndex) {
163+ insert(firstDelimiterIndex, dateInputFormat.delimiter.toString())
164+ }
165+ if (length >= secondDelimiterIndex) {
166+ insert(secondDelimiterIndex, dateInputFormat.delimiter.toString())
167+ }
168+ },
136169 )
137170
138171 if (selectableDates != null && showDatePickerModal) {
@@ -141,11 +174,8 @@ internal fun DatePickerItem(
141174 selectableDates,
142175 onDateSelected = { dateMillis ->
143176 dateMillis?.toLocalDate()?.let {
144- dateInputState =
145- DateInput (
146- display = it.format(dateInputFormat.patternWithoutDelimiters),
147- value = it,
148- )
177+ val formattedDate = it.format(dateInputFormat.patternWithoutDelimiters)
178+ textFieldState.setTextAndPlaceCursorAtEnd(formattedDate)
149179 }
150180 },
151181 ) {
@@ -197,4 +227,8 @@ typealias DateFormatPattern = String
197227
198228data class DateInput (val display : String , val value : LocalDate ? )
199229
230+ data class DateInputFormat (val patternWithDelimiters : String , val delimiter : Char ) {
231+ val patternWithoutDelimiters: String = patternWithDelimiters.replace(delimiter.toString(), " " )
232+ }
233+
200234const val DATE_TEXT_INPUT_FIELD = " date_picker_text_field"
0 commit comments