Skip to content

Commit 48f8fe4

Browse files
committed
Replace date visualTransformation with manual insert of delimiter
1 parent 9a26893 commit 48f8fe4

File tree

4 files changed

+53
-181
lines changed

4 files changed

+53
-181
lines changed

datacapture/src/main/java/com/google/android/fhir/datacapture/views/compose/DatePickerItem.kt

Lines changed: 48 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import androidx.compose.runtime.derivedStateOf
3434
import androidx.compose.runtime.getValue
3535
import androidx.compose.runtime.mutableStateOf
3636
import androidx.compose.runtime.remember
37+
import androidx.compose.runtime.rememberCoroutineScope
3738
import androidx.compose.runtime.setValue
3839
import androidx.compose.ui.Modifier
3940
import androidx.compose.ui.focus.FocusDirection
@@ -46,17 +47,17 @@ import androidx.compose.ui.res.stringResource
4647
import androidx.compose.ui.semantics.error
4748
import androidx.compose.ui.semantics.semantics
4849
import androidx.compose.ui.text.TextRange
49-
import androidx.compose.ui.text.buildAnnotatedString
5050
import androidx.compose.ui.text.input.ImeAction
5151
import androidx.compose.ui.text.input.KeyboardType
52-
import androidx.compose.ui.text.input.OffsetMapping
5352
import androidx.compose.ui.text.input.TextFieldValue
54-
import androidx.compose.ui.text.input.TransformedText
55-
import androidx.compose.ui.text.input.VisualTransformation
5653
import com.google.android.fhir.datacapture.R
5754
import com.google.android.fhir.datacapture.extensions.format
5855
import com.google.android.fhir.datacapture.extensions.toLocalDate
5956
import java.time.LocalDate
57+
import kotlinx.coroutines.Dispatchers
58+
import kotlinx.coroutines.Job
59+
import kotlinx.coroutines.delay
60+
import kotlinx.coroutines.launch
6061

6162
@OptIn(ExperimentalMaterial3Api::class)
6263
@Composable
@@ -76,43 +77,73 @@ internal fun DatePickerItem(
7677
val focusManager = LocalFocusManager.current
7778
val keyboardController = LocalSoftwareKeyboardController.current
7879
var dateInputState by remember(dateInput) { mutableStateOf(dateInput) }
80+
var deletionDetected by remember(dateInput) { mutableStateOf(false) }
81+
var typingJob by remember { mutableStateOf<Job?>(null) }
82+
val coroutineScope = rememberCoroutineScope { Dispatchers.Main }
7983
val dateInputDisplay by
8084
remember(dateInputState) {
8185
derivedStateOf {
86+
val text =
87+
if (!dateInputFormat.delimiterExistsInPattern || deletionDetected) {
88+
dateInputState.display
89+
} else {
90+
buildString {
91+
append(dateInputState.display)
92+
if (
93+
this.length > dateInputFormat.delimiterFirstIndex &&
94+
get(dateInputFormat.delimiterFirstIndex) != dateInputFormat.delimiter
95+
) {
96+
insert(dateInputFormat.delimiterFirstIndex, dateInputFormat.delimiter)
97+
}
98+
if (
99+
this.length > dateInputFormat.delimiterLastIndex &&
100+
dateInputFormat.delimiterLastIndex > dateInputFormat.delimiterFirstIndex &&
101+
get(dateInputFormat.delimiterLastIndex) != dateInputFormat.delimiter
102+
) {
103+
insert(dateInputFormat.delimiterLastIndex, dateInputFormat.delimiter)
104+
}
105+
}
106+
}
82107
TextFieldValue(
83-
text = dateInputState.display,
108+
text = text,
84109
selection = TextRange(dateInputFormat.patternWithDelimiters.length),
85110
)
86111
}
87112
}
88113

89114
var showDatePickerModal by remember { mutableStateOf(false) }
90115

91-
LaunchedEffect(dateInputState) {
92-
if (dateInputState != dateInput) {
93-
onDateInputEntry(dateInputState)
94-
}
95-
}
96-
97116
OutlinedTextField(
98117
value = dateInputDisplay,
99118
onValueChange = {
100119
val text = it.text
120+
deletionDetected = text.length < dateInputState.display.length
101121
if (
102-
text.length <= dateInputFormat.patternWithoutDelimiters.length &&
103-
text.all { char -> char.isDigit() }
122+
text.length <= dateInputFormat.patternWithDelimiters.length &&
123+
text.all { char -> char.isDigit() || char == dateInputFormat.delimiter }
104124
) {
105125
val trimmedText = text.trim()
106126
val localDate =
107127
if (
108128
trimmedText.isNotBlank() &&
109-
trimmedText.length == dateInputFormat.patternWithoutDelimiters.length
129+
trimmedText.length == dateInputFormat.patternWithDelimiters.length
110130
) {
111-
parseStringToLocalDate(trimmedText, dateInputFormat.patternWithoutDelimiters)
131+
parseStringToLocalDate(trimmedText, dateInputFormat.patternWithDelimiters)
112132
} else {
113133
null
114134
}
115-
dateInputState = DateInput(text, localDate)
135+
val newDateInput = DateInput(text, localDate)
136+
dateInputState = newDateInput
137+
138+
typingJob?.cancel() // Cancel previous debounce
139+
typingJob =
140+
coroutineScope.launch {
141+
delay(HANDLE_INPUT_DEBOUNCE_TIME) // Debounce delay (e.g., 500ms)
142+
if (newDateInput != dateInput) {
143+
onDateInputEntry(newDateInput)
144+
}
145+
// Perform actions after user stops typing
146+
}
116147
}
117148
},
118149
singleLine = true,
@@ -147,25 +178,6 @@ internal fun DatePickerItem(
147178
KeyboardActions(
148179
onNext = { focusManager.moveFocus(FocusDirection.Down) },
149180
),
150-
visualTransformation =
151-
if (!dateInputFormat.delimiterExistsInPattern) {
152-
VisualTransformation.None
153-
} else {
154-
VisualTransformation { originalText ->
155-
val text = buildAnnotatedString {
156-
originalText.forEachIndexed { index, ch ->
157-
append(ch)
158-
if (
159-
index + 1 == dateInputFormat.delimiterFirstIndex ||
160-
index + 2 == dateInputFormat.delimiterLastIndex
161-
) {
162-
append(dateInputFormat.delimiter)
163-
}
164-
}
165-
}
166-
TransformedText(text, dateInputFormat.offsetMapping)
167-
}
168-
},
169181
)
170182

171183
if (selectableDates != null && showDatePickerModal) {
@@ -176,7 +188,7 @@ internal fun DatePickerItem(
176188
dateMillis?.toLocalDate()?.let {
177189
dateInputState =
178190
DateInput(
179-
display = it.format(dateInputFormat.patternWithoutDelimiters),
191+
display = it.format(dateInputFormat.patternWithDelimiters),
180192
value = it,
181193
)
182194
}
@@ -236,29 +248,6 @@ data class DateInputFormat(val patternWithDelimiters: String, val delimiter: Cha
236248
val delimiterFirstIndex: Int = patternWithDelimiters.indexOf(delimiter)
237249
val delimiterLastIndex: Int = patternWithDelimiters.lastIndexOf(delimiter)
238250
val delimiterExistsInPattern = delimiterFirstIndex != -1 && delimiterLastIndex != -1
239-
240-
val offsetMapping =
241-
object : OffsetMapping {
242-
override fun originalToTransformed(offset: Int): Int {
243-
return when {
244-
delimiterExistsInPattern &&
245-
offset >= delimiterLastIndex &&
246-
delimiterLastIndex > delimiterFirstIndex -> offset + 2
247-
delimiterExistsInPattern && offset >= delimiterFirstIndex -> offset + 1
248-
else -> offset
249-
}
250-
}
251-
252-
override fun transformedToOriginal(offset: Int): Int {
253-
return when {
254-
delimiterExistsInPattern &&
255-
offset >= delimiterLastIndex &&
256-
offset > delimiterFirstIndex -> offset - 2
257-
delimiterExistsInPattern && offset >= delimiterFirstIndex -> offset - 1
258-
else -> offset
259-
}
260-
}
261-
}
262251
}
263252

264253
const val DATE_TEXT_INPUT_FIELD = "date_picker_text_field"

datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DatePickerViewHolderFactory.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ internal object DatePickerViewHolderFactory : QuestionnaireItemComposeViewHolder
101101
remember(questionnaireViewItem) { questionnaireViewItem.draftAnswer as? String }
102102
val dateInput =
103103
remember(dateInputFormat, questionnaireItemAnswerLocalDate, draftAnswer) {
104-
questionnaireItemAnswerLocalDate
105-
?.format(dateInputFormat.patternWithoutDelimiters)
106-
?.let { DateInput(it, questionnaireItemAnswerLocalDate) }
104+
questionnaireItemAnswerLocalDate?.format(dateInputFormat.patternWithDelimiters)?.let {
105+
DateInput(it, questionnaireItemAnswerLocalDate)
106+
}
107107
?: DateInput(display = draftAnswer ?: "", null)
108108
}
109109

@@ -168,7 +168,7 @@ internal object DatePickerViewHolderFactory : QuestionnaireItemComposeViewHolder
168168
parseDateOnTextChanged(
169169
questionnaireViewItem,
170170
display,
171-
dateInputFormat.patternWithoutDelimiters,
171+
dateInputFormat.patternWithDelimiters,
172172
)
173173
}
174174
}

datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DateTimePickerViewHolderFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ internal object DateTimePickerViewHolderFactory : QuestionnaireItemComposeViewHo
129129
remember(questionnaireViewItem) { questionnaireViewItem.draftAnswer as? String }
130130
val dateInput =
131131
remember(dateInputFormat, questionnaireItemViewItemDate, draftAnswer) {
132-
questionnaireItemViewItemDate?.format(dateInputFormat.patternWithoutDelimiters)?.let {
132+
questionnaireItemViewItemDate?.format(dateInputFormat.patternWithDelimiters)?.let {
133133
DateInput(it, questionnaireItemViewItemDate)
134134
}
135135
?: DateInput(display = draftAnswer ?: "", null)

datacapture/src/test/java/com/google/android/fhir/datacapture/views/compose/DateVisualTransformationTest.kt

Lines changed: 0 additions & 117 deletions
This file was deleted.

0 commit comments

Comments
 (0)