Skip to content

Commit 639d7e4

Browse files
committed
WIP
1 parent e54e7b7 commit 639d7e4

File tree

7 files changed

+385
-60
lines changed

7 files changed

+385
-60
lines changed

datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/DropDownViewHolderFactoryEspressoTest.kt

Lines changed: 57 additions & 43 deletions
Large diffs are not rendered by default.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import androidx.test.core.app.ApplicationProvider
2828
import androidx.test.ext.junit.runners.AndroidJUnit4
2929
import com.google.android.fhir.datacapture.R
3030
import com.google.android.fhir.datacapture.views.compose.DROP_DOWN_TEXT_FIELD_TAG
31-
import com.google.android.fhir.datacapture.views.compose.ExposedDropDownMenuBoxItem
31+
import com.google.android.fhir.datacapture.views.compose.ReadOnlyExposedDropDownMenuBoxItem
3232
import com.google.android.fhir.datacapture.views.factories.DropDownAnswerOption
3333
import org.junit.Rule
3434
import org.junit.Test
@@ -51,7 +51,7 @@ class ExposedDropDownMenuBoxItemTest {
5151
)
5252

5353
composeTestRule.setContent {
54-
ExposedDropDownMenuBoxItem(
54+
ReadOnlyExposedDropDownMenuBoxItem(
5555
modifier = Modifier,
5656
enabled = true,
5757
options = listOf(testDropDownAnswerOption),

datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemAnswerOptionComponents.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2023 Google LLC
2+
* Copyright 2022-2025 Google LLC
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -61,6 +61,7 @@ fun Questionnaire.QuestionnaireItemAnswerOptionComponent.itemAnswerOptionImage(
6161
"image/jpg",
6262
"image/png", -> {
6363
val bitmap = BitmapFactory.decodeByteArray(it.data, 0, it.data.size)
64+
println("Bitmap is null: ${bitmap == null}")
6465
val imageSize =
6566
context.resources.getDimensionPixelOffset(R.dimen.item_answer_media_image_size)
6667
val drawable: Drawable = BitmapDrawable(context.resources, bitmap)

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

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import com.google.android.fhir.datacapture.views.factories.DropDownAnswerOption
4949

5050
@OptIn(ExperimentalMaterial3Api::class)
5151
@Composable
52-
internal fun ExposedDropDownMenuBoxItem(
52+
internal fun ReadOnlyExposedDropDownMenuBoxItem(
5353
modifier: Modifier,
5454
enabled: Boolean,
5555
labelText: AnnotatedString? = null,
@@ -106,6 +106,7 @@ internal fun ExposedDropDownMenuBoxItem(
106106
if (showClearIcon) {
107107
IconButton(
108108
onClick = { selectedDropDownAnswerOption = null },
109+
enabled = enabled,
109110
modifier = Modifier.testTag(CLEAR_TEXT_ICON_BUTTON_TAG),
110111
) {
111112
Icon(painterResource(R.drawable.ic_clear), contentDescription = "clear")
@@ -143,6 +144,208 @@ internal fun ExposedDropDownMenuBoxItem(
143144
}
144145
}
145146

147+
@OptIn(ExperimentalMaterial3Api::class)
148+
@Composable
149+
internal fun FilterableExposedDropDownMenuBoxItem(
150+
modifier: Modifier,
151+
enabled: Boolean,
152+
labelText: AnnotatedString? = null,
153+
supportingText: String? = null,
154+
isError: Boolean = false,
155+
showClearIcon: Boolean = false,
156+
readOnly: Boolean = showClearIcon,
157+
selectedOption: DropDownAnswerOption? = null,
158+
options: List<DropDownAnswerOption>,
159+
onDropDownAnswerOptionSelected: (DropDownAnswerOption?) -> Unit,
160+
) {
161+
var expanded by remember { mutableStateOf(false) }
162+
var selectedDropDownAnswerOption by
163+
remember(selectedOption, options) { mutableStateOf(selectedOption) }
164+
var selectedOptionDisplay by
165+
remember(selectedDropDownAnswerOption) {
166+
mutableStateOf(selectedDropDownAnswerOption?.answerOptionString ?: "")
167+
}
168+
169+
LaunchedEffect(selectedDropDownAnswerOption) {
170+
onDropDownAnswerOptionSelected(selectedDropDownAnswerOption)
171+
}
172+
173+
ExposedDropdownMenuBox(
174+
modifier = modifier,
175+
expanded = expanded,
176+
onExpandedChange = { expanded = it },
177+
) {
178+
OutlinedTextField(
179+
value = selectedOptionDisplay,
180+
onValueChange = { selectedOptionDisplay = it },
181+
modifier =
182+
Modifier.fillMaxWidth()
183+
.testTag(DROP_DOWN_TEXT_FIELD_TAG)
184+
.semantics { if (isError) error(supportingText ?: "") }
185+
.menuAnchor(MenuAnchorType.PrimaryEditable, enabled),
186+
readOnly = readOnly,
187+
enabled = enabled,
188+
minLines = 1,
189+
isError = isError,
190+
label = { labelText?.let { Text(it) } },
191+
supportingText = { supportingText?.let { Text(it) } },
192+
leadingIcon =
193+
selectedDropDownAnswerOption?.answerOptionImage?.let {
194+
{
195+
Icon(
196+
it.toBitmap().asImageBitmap(),
197+
contentDescription = selectedOptionDisplay,
198+
modifier = Modifier.testTag(DROP_DOWN_TEXT_FIELD_LEADING_ICON_TAG),
199+
)
200+
}
201+
},
202+
trailingIcon = {
203+
Row(verticalAlignment = Alignment.CenterVertically) {
204+
if (showClearIcon) {
205+
IconButton(
206+
onClick = { selectedDropDownAnswerOption = null },
207+
modifier = Modifier.testTag(CLEAR_TEXT_ICON_BUTTON_TAG),
208+
) {
209+
Icon(painterResource(R.drawable.ic_clear), contentDescription = "clear")
210+
}
211+
}
212+
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded, modifier = Modifier.menuAnchor(MenuAnchorType.SecondaryEditable, enabled))
213+
}
214+
},
215+
)
216+
217+
val filteredOptions = options.filter { it.answerOptionString.contains(selectedOptionDisplay, true) }
218+
if (filteredOptions.isNotEmpty()) {
219+
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
220+
filteredOptions.forEach { option ->
221+
DropdownMenuItem(
222+
modifier = Modifier.testTag(DROP_DOWN_MENU_ITEM_TAG),
223+
text = {
224+
Text(option.answerOptionAnnotatedString(), style = MaterialTheme.typography.bodyLarge)
225+
},
226+
leadingIcon =
227+
option.answerOptionImage?.let {
228+
{
229+
Icon(
230+
it.toBitmap().asImageBitmap(),
231+
contentDescription = option.answerOptionString,
232+
)
233+
}
234+
},
235+
enabled = enabled,
236+
onClick = {
237+
selectedDropDownAnswerOption = option
238+
expanded = false
239+
},
240+
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
241+
)
242+
}
243+
}
244+
}
245+
}
246+
}
247+
248+
@OptIn(ExperimentalMaterial3Api::class)
249+
@Composable
250+
internal fun AutoCompleteExposedDropDownMenuBoxItem(
251+
modifier: Modifier,
252+
enabled: Boolean,
253+
labelText: AnnotatedString? = null,
254+
supportingText: String? = null,
255+
isError: Boolean = false,
256+
showClearIcon: Boolean = false,
257+
readOnly: Boolean = showClearIcon,
258+
selectedOption: DropDownAnswerOption? = null,
259+
options: List<DropDownAnswerOption>,
260+
onDropDownAnswerOptionSelected: (DropDownAnswerOption?) -> Unit,
261+
) {
262+
var expanded by remember { mutableStateOf(false) }
263+
var selectedDropDownAnswerOption by
264+
remember(selectedOption, options) { mutableStateOf(selectedOption) }
265+
var selectedOptionDisplay by
266+
remember(selectedDropDownAnswerOption) {
267+
mutableStateOf(selectedDropDownAnswerOption?.answerOptionString ?: "")
268+
}
269+
270+
LaunchedEffect(selectedDropDownAnswerOption) {
271+
onDropDownAnswerOptionSelected(selectedDropDownAnswerOption)
272+
}
273+
274+
ExposedDropdownMenuBox(
275+
modifier = modifier,
276+
expanded = expanded,
277+
onExpandedChange = { expanded = it },
278+
) {
279+
OutlinedTextField(
280+
value = selectedOptionDisplay,
281+
onValueChange = { selectedOptionDisplay = it },
282+
modifier =
283+
Modifier.fillMaxWidth()
284+
.testTag(DROP_DOWN_TEXT_FIELD_TAG)
285+
.semantics { if (isError) error(supportingText ?: "") }
286+
.menuAnchor(MenuAnchorType.PrimaryEditable, enabled),
287+
readOnly = readOnly,
288+
enabled = enabled,
289+
minLines = 1,
290+
isError = isError,
291+
label = { labelText?.let { Text(it) } },
292+
supportingText = { supportingText?.let { Text(it) } },
293+
leadingIcon =
294+
selectedDropDownAnswerOption?.answerOptionImage?.let {
295+
{
296+
Icon(
297+
it.toBitmap().asImageBitmap(),
298+
contentDescription = selectedOptionDisplay,
299+
modifier = Modifier.testTag(DROP_DOWN_TEXT_FIELD_LEADING_ICON_TAG),
300+
)
301+
}
302+
},
303+
trailingIcon = {
304+
Row(verticalAlignment = Alignment.CenterVertically) {
305+
if (showClearIcon) {
306+
IconButton(
307+
onClick = { selectedDropDownAnswerOption = null },
308+
modifier = Modifier.testTag(CLEAR_TEXT_ICON_BUTTON_TAG),
309+
) {
310+
Icon(painterResource(R.drawable.ic_clear), contentDescription = "clear")
311+
}
312+
}
313+
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded, modifier = Modifier.menuAnchor(MenuAnchorType.SecondaryEditable, enabled))
314+
}
315+
},
316+
)
317+
318+
val filteredOptions = options.filter { it.answerOptionString.contains(selectedOptionDisplay, true) }
319+
if (filteredOptions.isNotEmpty()) {
320+
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
321+
filteredOptions.forEach { option ->
322+
DropdownMenuItem(
323+
modifier = Modifier.testTag(DROP_DOWN_MENU_ITEM_TAG),
324+
text = {
325+
Text(option.answerOptionAnnotatedString(), style = MaterialTheme.typography.bodyLarge)
326+
},
327+
leadingIcon =
328+
option.answerOptionImage?.let {
329+
{
330+
Icon(
331+
it.toBitmap().asImageBitmap(),
332+
contentDescription = option.answerOptionString,
333+
)
334+
}
335+
},
336+
enabled = enabled,
337+
onClick = {
338+
selectedDropDownAnswerOption = option
339+
expanded = false
340+
},
341+
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
342+
)
343+
}
344+
}
345+
}
346+
}
347+
}
348+
146349
const val CLEAR_TEXT_ICON_BUTTON_TAG = "clear_field_text"
147350
const val DROP_DOWN_TEXT_FIELD_TAG = "drop_down_text_field"
148351
const val DROP_DOWN_TEXT_FIELD_LEADING_ICON_TAG = "drop_down_text_field_leading_icon"

0 commit comments

Comments
 (0)