Skip to content

Commit 9a26893

Browse files
committed
Use TextFieldValue instead to set and manage cursor position
1 parent a102791 commit 9a26893

File tree

3 files changed

+73
-85
lines changed

3 files changed

+73
-85
lines changed

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

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,14 @@ import androidx.compose.ui.res.painterResource
4545
import androidx.compose.ui.res.stringResource
4646
import androidx.compose.ui.semantics.error
4747
import androidx.compose.ui.semantics.semantics
48+
import androidx.compose.ui.text.TextRange
49+
import androidx.compose.ui.text.buildAnnotatedString
4850
import androidx.compose.ui.text.input.ImeAction
4951
import androidx.compose.ui.text.input.KeyboardType
52+
import androidx.compose.ui.text.input.OffsetMapping
53+
import androidx.compose.ui.text.input.TextFieldValue
54+
import androidx.compose.ui.text.input.TransformedText
55+
import androidx.compose.ui.text.input.VisualTransformation
5056
import com.google.android.fhir.datacapture.R
5157
import com.google.android.fhir.datacapture.extensions.format
5258
import com.google.android.fhir.datacapture.extensions.toLocalDate
@@ -70,7 +76,15 @@ internal fun DatePickerItem(
7076
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 dateInputDisplay by
80+
remember(dateInputState) {
81+
derivedStateOf {
82+
TextFieldValue(
83+
text = dateInputState.display,
84+
selection = TextRange(dateInputFormat.patternWithDelimiters.length),
85+
)
86+
}
87+
}
7488

7589
var showDatePickerModal by remember { mutableStateOf(false) }
7690

@@ -83,11 +97,12 @@ internal fun DatePickerItem(
8397
OutlinedTextField(
8498
value = dateInputDisplay,
8599
onValueChange = {
100+
val text = it.text
86101
if (
87-
it.length <= dateInputFormat.patternWithoutDelimiters.length &&
88-
it.all { char -> char.isDigit() }
102+
text.length <= dateInputFormat.patternWithoutDelimiters.length &&
103+
text.all { char -> char.isDigit() }
89104
) {
90-
val trimmedText = it.trim()
105+
val trimmedText = text.trim()
91106
val localDate =
92107
if (
93108
trimmedText.isNotBlank() &&
@@ -97,7 +112,7 @@ internal fun DatePickerItem(
97112
} else {
98113
null
99114
}
100-
dateInputState = DateInput(it, localDate)
115+
dateInputState = DateInput(text, localDate)
101116
}
102117
},
103118
singleLine = true,
@@ -132,7 +147,25 @@ internal fun DatePickerItem(
132147
KeyboardActions(
133148
onNext = { focusManager.moveFocus(FocusDirection.Down) },
134149
),
135-
visualTransformation = DateVisualTransformation(dateInputFormat),
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+
},
136169
)
137170

138171
if (selectableDates != null && showDatePickerModal) {
@@ -197,4 +230,35 @@ typealias DateFormatPattern = String
197230

198231
data class DateInput(val display: String, val value: LocalDate?)
199232

233+
data class DateInputFormat(val patternWithDelimiters: String, val delimiter: Char) {
234+
val patternWithoutDelimiters: String = patternWithDelimiters.replace(delimiter.toString(), "")
235+
236+
val delimiterFirstIndex: Int = patternWithDelimiters.indexOf(delimiter)
237+
val delimiterLastIndex: Int = patternWithDelimiters.lastIndexOf(delimiter)
238+
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+
}
262+
}
263+
200264
const val DATE_TEXT_INPUT_FIELD = "date_picker_text_field"

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

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

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.android.fhir.datacapture.views.compose
1818

19+
import androidx.compose.foundation.layout.fillMaxWidth
1920
import androidx.compose.foundation.text.KeyboardActions
2021
import androidx.compose.foundation.text.KeyboardOptions
2122
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -69,6 +70,7 @@ internal fun TimePickerItem(
6970
var expanded by remember { mutableStateOf(false) }
7071

7172
ExposedDropdownMenuBox(
73+
modifier = modifier,
7274
expanded = expanded,
7375
onExpandedChange = {
7476
if (it) {
@@ -83,7 +85,7 @@ internal fun TimePickerItem(
8385
singleLine = true,
8486
label = { Text(hint) },
8587
modifier =
86-
modifier
88+
Modifier.fillMaxWidth()
8789
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled)
8890
.testTag(TIME_PICKER_INPUT_FIELD)
8991
.onFocusChanged {

0 commit comments

Comments
 (0)