Skip to content

Commit e93235f

Browse files
committed
Update DropDown compose component for QuantityViewHolderFactory
1 parent 1c64943 commit e93235f

File tree

6 files changed

+111
-47
lines changed

6 files changed

+111
-47
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ 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.DropDownItem
3232
import com.google.android.fhir.datacapture.views.factories.DropDownAnswerOption
3333
import org.junit.Rule
3434
import org.junit.Test
3535
import org.junit.runner.RunWith
3636

3737
@RunWith(AndroidJUnit4::class)
38-
class ExposedDropDownMenuBoxItemTest {
38+
class DropDownItemTest {
3939

4040
@get:Rule val composeTestRule = createComposeRule()
4141

@@ -51,7 +51,7 @@ class ExposedDropDownMenuBoxItemTest {
5151
)
5252

5353
composeTestRule.setContent {
54-
ExposedDropDownMenuBoxItem(
54+
DropDownItem(
5555
modifier = Modifier,
5656
enabled = true,
5757
options = listOf(testDropDownAnswerOption),
Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616

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

19+
import androidx.compose.foundation.layout.fillMaxWidth
1920
import androidx.compose.material3.DropdownMenuItem
2021
import androidx.compose.material3.ExperimentalMaterial3Api
22+
import androidx.compose.material3.ExposedDropdownMenuAnchorType
2123
import androidx.compose.material3.ExposedDropdownMenuBox
2224
import androidx.compose.material3.ExposedDropdownMenuDefaults
2325
import androidx.compose.material3.Icon
2426
import androidx.compose.material3.MaterialTheme
25-
import androidx.compose.material3.MenuAnchorType
2627
import androidx.compose.material3.OutlinedTextField
2728
import androidx.compose.material3.Text
2829
import androidx.compose.runtime.Composable
@@ -35,14 +36,20 @@ import androidx.compose.runtime.setValue
3536
import androidx.compose.ui.Modifier
3637
import androidx.compose.ui.graphics.asImageBitmap
3738
import androidx.compose.ui.platform.testTag
39+
import androidx.compose.ui.semantics.error
40+
import androidx.compose.ui.semantics.semantics
41+
import androidx.compose.ui.text.AnnotatedString
3842
import androidx.core.graphics.drawable.toBitmap
3943
import com.google.android.fhir.datacapture.views.factories.DropDownAnswerOption
4044

4145
@OptIn(ExperimentalMaterial3Api::class)
4246
@Composable
43-
internal fun ExposedDropDownMenuBoxItem(
47+
internal fun DropDownItem(
4448
modifier: Modifier,
4549
enabled: Boolean,
50+
labelText: AnnotatedString? = null,
51+
supportingText: String? = null,
52+
isError: Boolean = false,
4653
selectedOption: DropDownAnswerOption? = null,
4754
options: List<DropDownAnswerOption>,
4855
onDropDownAnswerOptionSelected: (DropDownAnswerOption?) -> Unit,
@@ -68,39 +75,66 @@ internal fun ExposedDropDownMenuBoxItem(
6875
value = selectedOptionDisplay,
6976
onValueChange = {},
7077
modifier =
71-
Modifier.testTag(DROP_DOWN_TEXT_FIELD_TAG)
72-
.menuAnchor(MenuAnchorType.PrimaryNotEditable, enabled),
78+
Modifier.fillMaxWidth()
79+
.testTag(DROP_DOWN_TEXT_FIELD_TAG)
80+
.semantics { if (isError) error(supportingText ?: "") }
81+
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
7382
readOnly = true,
7483
enabled = enabled,
7584
minLines = 1,
76-
label = {},
77-
supportingText = {},
85+
isError = isError,
86+
label = { labelText?.let { Text(it) } },
87+
supportingText = { supportingText?.let { Text(it) } },
88+
leadingIcon =
89+
selectedDropDownAnswerOption?.answerOptionImage?.let {
90+
{
91+
Icon(
92+
it.toBitmap().asImageBitmap(),
93+
contentDescription = selectedOptionDisplay,
94+
modifier = Modifier.testTag(DROP_DOWN_TEXT_FIELD_LEADING_ICON_TAG),
95+
)
96+
}
97+
},
7898
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
7999
)
80100
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
81101
options.forEach { option ->
82-
DropdownMenuItem(
83-
text = {
84-
Text(option.answerOptionAnnotatedString(), style = MaterialTheme.typography.bodyLarge)
85-
},
86-
leadingIcon = {
87-
option.answerOptionImage?.let {
88-
Icon(
89-
it.toBitmap().asImageBitmap(),
90-
contentDescription = option.answerOptionString,
91-
)
92-
}
93-
},
94-
enabled = enabled,
95-
onClick = {
96-
selectedDropDownAnswerOption = option
97-
expanded = false
98-
},
99-
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
100-
)
102+
DropDownAnswerMenuItem(enabled, option) {
103+
selectedDropDownAnswerOption = option
104+
expanded = false
105+
}
101106
}
102107
}
103108
}
104109
}
105110

111+
@OptIn(ExperimentalMaterial3Api::class)
112+
@Composable
113+
internal fun DropDownAnswerMenuItem(
114+
enabled: Boolean,
115+
answerOption: DropDownAnswerOption,
116+
onSelected: () -> Unit,
117+
) {
118+
DropdownMenuItem(
119+
modifier = Modifier.testTag(DROP_DOWN_ANSWER_MENU_ITEM_TAG),
120+
text = {
121+
Text(answerOption.answerOptionAnnotatedString(), style = MaterialTheme.typography.bodyLarge)
122+
},
123+
leadingIcon =
124+
answerOption.answerOptionImage?.let {
125+
{
126+
Icon(
127+
it.toBitmap().asImageBitmap(),
128+
contentDescription = answerOption.answerOptionString,
129+
)
130+
}
131+
},
132+
enabled = enabled,
133+
onClick = { onSelected() },
134+
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
135+
)
136+
}
137+
106138
const val DROP_DOWN_TEXT_FIELD_TAG = "drop_down_text_field"
139+
const val DROP_DOWN_TEXT_FIELD_LEADING_ICON_TAG = "drop_down_text_field_leading_icon"
140+
const val DROP_DOWN_ANSWER_MENU_ITEM_TAG = "drop_down_answer_list_menu_item"

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ import com.google.android.fhir.datacapture.validation.NotValidated
4747
import com.google.android.fhir.datacapture.validation.Valid
4848
import com.google.android.fhir.datacapture.validation.ValidationResult
4949
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
50+
import com.google.android.fhir.datacapture.views.compose.DropDownItem
5051
import com.google.android.fhir.datacapture.views.compose.EditTextFieldItem
5152
import com.google.android.fhir.datacapture.views.compose.EditTextFieldState
52-
import com.google.android.fhir.datacapture.views.compose.ExposedDropDownMenuBoxItem
5353
import com.google.android.fhir.datacapture.views.compose.Header
5454
import com.google.android.fhir.datacapture.views.compose.MediaItem
5555
import java.math.BigDecimal
@@ -77,6 +77,7 @@ internal object QuantityViewHolderFactory : QuestionnaireItemComposeViewHolderFa
7777
val selectedOption =
7878
remember(questionnaireViewItem) {
7979
unitTextCoding(questionnaireViewItem)?.toDropDownAnswerOption()
80+
?: dropDownOptions.singleOrNull() // Select if has only one option
8081
}
8182

8283
var quantity by
@@ -118,16 +119,17 @@ internal object QuantityViewHolderFactory : QuestionnaireItemComposeViewHolderFa
118119
Header(questionnaireViewItem)
119120
questionnaireViewItem.questionnaireItem.itemMedia?.let { MediaItem(it) }
120121

121-
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
122+
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Top) {
122123
EditTextFieldItem(
123124
modifier = Modifier.weight(1f),
124125
textFieldState = composeViewQuestionnaireState,
125126
)
126127
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.item_margin_horizontal)))
127-
ExposedDropDownMenuBoxItem(
128+
DropDownItem(
128129
modifier = Modifier.weight(1f),
129130
enabled = !isReadOnly,
130131
selectedOption = selectedOption,
132+
isError = !validationUiMessage.isNullOrBlank(),
131133
options = dropDownOptions,
132134
) { answerOption ->
133135
quantity = UiQuantity(quantity.value, answerOption?.findCoding(unitOptions))
@@ -147,14 +149,27 @@ internal object QuantityViewHolderFactory : QuestionnaireItemComposeViewHolderFa
147149
questionnaireViewItem: QuestionnaireViewItem,
148150
input: UiQuantity,
149151
) {
150-
val currentAnswerQuantity = questionnaireViewItem.answers.singleOrNull()?.valueQuantity
151-
val draftAnswer = questionnaireViewItem.draftAnswer
152-
153-
val decimal =
154-
input.value?.toBigDecimalOrNull()
155-
?: (draftAnswer as? BigDecimal) ?: currentAnswerQuantity?.value
156-
val unit =
157-
input.unitDropDown ?: ((draftAnswer as? Coding) ?: currentAnswerQuantity?.toCoding())
152+
var decimal: BigDecimal? = null
153+
var unit: Coding? = null
154+
155+
// Read decimal value and unit from complete answer
156+
questionnaireViewItem.answers.singleOrNull()?.let {
157+
val quantity = it.value as Quantity
158+
decimal = quantity.value
159+
unit = quantity.toCoding()
160+
}
161+
162+
// Read decimal value and unit from partial answer
163+
questionnaireViewItem.draftAnswer?.let {
164+
when (it) {
165+
is BigDecimal -> decimal = it
166+
is Coding -> unit = it
167+
}
168+
}
169+
170+
// Update decimal value and unit
171+
input.value?.let { decimal = it.toBigDecimalOrNull() }
172+
input.unitDropDown?.let { unit = it }
158173

159174
when {
160175
decimal == null && unit == null -> {

engine/benchmarks/app/src/main/java/com/google/android/fhir/engine/benchmarks/app/ui/DetailScaffold.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ package com.google.android.fhir.engine.benchmarks.app.ui
1919
import androidx.compose.foundation.layout.fillMaxSize
2020
import androidx.compose.foundation.layout.padding
2121
import androidx.compose.foundation.shape.RoundedCornerShape
22-
import androidx.compose.material.icons.Icons
23-
import androidx.compose.material.icons.automirrored.filled.ArrowBack
24-
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
2522
import androidx.compose.material3.Card
2623
import androidx.compose.material3.CardDefaults
2724
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -33,10 +30,12 @@ import androidx.compose.material3.TopAppBar
3330
import androidx.compose.runtime.Composable
3431
import androidx.compose.ui.Modifier
3532
import androidx.compose.ui.graphics.Color
33+
import androidx.compose.ui.res.painterResource
3634
import androidx.compose.ui.semantics.semantics
3735
import androidx.compose.ui.semantics.testTagsAsResourceId
3836
import androidx.compose.ui.tooling.preview.Preview
3937
import androidx.compose.ui.unit.dp
38+
import com.google.android.fhir.engine.benchmarks.app.R
4039

4140
@OptIn(ExperimentalMaterial3Api::class)
4241
@Composable
@@ -52,8 +51,8 @@ fun DetailScaffold(
5251
navigationIcon = {
5352
IconButton(onClick = navigateToHome) {
5453
Icon(
55-
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
56-
contentDescription = "Localized description",
54+
painterResource(R.drawable.arrow_back_24px),
55+
contentDescription = "Back",
5756
)
5857
}
5958
},
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<vector
2+
xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:autoMirrored="true"
4+
android:height="24dp"
5+
android:tint="#1A73E8"
6+
android:viewportHeight="960"
7+
android:viewportWidth="960"
8+
android:width="24dp"
9+
>
10+
11+
<path
12+
android:fillColor="@android:color/white"
13+
android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"
14+
/>
15+
16+
</vector>

gradle/libs.versions.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ android-fhir-common = "0.1.0-alpha05"
66
android-fhir-engine = "0.1.0-beta05"
77
android-fhir-knowledge = "0.1.0-beta01"
88
androidx-activity = "1.7.2"
9-
androidx-activity-compose = "1.10.1"
9+
androidx-activity-compose = "1.11.0"
1010
androidx-appcompat = "1.6.1"
1111
androidx-arch-core = "2.2.0"
1212
androidx-benchmark = "1.4.0-rc01"
1313
androidx-benchmark-macro = "1.4.0-rc01"
14-
androidx-compose-bom = "2025.07.00"
14+
androidx-compose-bom = "2025.10.01"
1515
androidx-constraintlayout = "2.1.4"
1616
androidx-core = "1.10.1"
1717
androidx-datastore = "1.0.0"
1818
androidx-espresso = "3.5.1"
1919
androidx-fragment = "1.6.0"
2020
androidx-lifecycle = "2.8.7"
2121
androidx-navigation = "2.6.0"
22-
androidx-navigation-compose = "2.8.9"
22+
androidx-navigation-compose = "2.9.5"
2323
androidx-profilerinstaller = "1.4.1"
2424
androidx-recyclerview = "1.4.0"
2525
androidx-room = "2.7.1"

0 commit comments

Comments
 (0)