Skip to content

Commit 6f0ce2c

Browse files
committed
Cleanup migration of DropDownViewHolderFactory
1 parent e54e7b7 commit 6f0ce2c

File tree

10 files changed

+204
-340
lines changed

10 files changed

+204
-340
lines changed

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

Lines changed: 44 additions & 54 deletions
Large diffs are not rendered by default.

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import com.google.android.fhir.datacapture.validation.NotValidated
4545
import com.google.android.fhir.datacapture.views.QuestionTextConfiguration
4646
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
4747
import com.google.android.fhir.datacapture.views.compose.CLEAR_TEXT_ICON_BUTTON_TAG
48-
import com.google.android.fhir.datacapture.views.compose.DROP_DOWN_MENU_ITEM_TAG
48+
import com.google.android.fhir.datacapture.views.compose.DROP_DOWN_ANSWER_MENU_ITEM_TAG
4949
import com.google.android.fhir.datacapture.views.compose.DROP_DOWN_TEXT_FIELD_TAG
5050
import com.google.android.fhir.datacapture.views.compose.ERROR_TEXT_AT_HEADER_TEST_TAG
5151
import com.google.android.fhir.datacapture.views.factories.DropDownViewHolderFactory
@@ -117,7 +117,7 @@ class DropDownViewHolderFactoryTest {
117117
.onNode(isPopup())
118118
.assert(
119119
hasAnyDescendant(
120-
hasTestTag(DROP_DOWN_MENU_ITEM_TAG) and hasTextExactly("Test Code"),
120+
hasTestTag(DROP_DOWN_ANSWER_MENU_ITEM_TAG) and hasTextExactly("Test Code"),
121121
),
122122
)
123123
}
@@ -145,7 +145,7 @@ class DropDownViewHolderFactoryTest {
145145
.onNode(isPopup())
146146
.assert(
147147
hasAnyDescendant(
148-
hasTestTag(DROP_DOWN_MENU_ITEM_TAG) and hasTextExactly("John Doe"),
148+
hasTestTag(DROP_DOWN_ANSWER_MENU_ITEM_TAG) and hasTextExactly("John Doe"),
149149
),
150150
)
151151
}
@@ -169,7 +169,7 @@ class DropDownViewHolderFactoryTest {
169169
.onNode(isPopup())
170170
.assert(
171171
hasAnyDescendant(
172-
hasTestTag(DROP_DOWN_MENU_ITEM_TAG) and hasTextExactly("Patient/123"),
172+
hasTestTag(DROP_DOWN_ANSWER_MENU_ITEM_TAG) and hasTextExactly("Patient/123"),
173173
),
174174
)
175175
}
@@ -193,7 +193,7 @@ class DropDownViewHolderFactoryTest {
193193
.onNode(isPopup())
194194
.assert(
195195
hasAnyDescendant(
196-
hasTestTag(DROP_DOWN_MENU_ITEM_TAG) and hasTextExactly("test-code"),
196+
hasTestTag(DROP_DOWN_ANSWER_MENU_ITEM_TAG) and hasTextExactly("test-code"),
197197
),
198198
)
199199
}
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: 124 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ import androidx.compose.foundation.layout.Row
2020
import androidx.compose.foundation.layout.fillMaxWidth
2121
import androidx.compose.material3.DropdownMenuItem
2222
import androidx.compose.material3.ExperimentalMaterial3Api
23+
import androidx.compose.material3.ExposedDropdownMenuAnchorType
2324
import androidx.compose.material3.ExposedDropdownMenuBox
2425
import androidx.compose.material3.ExposedDropdownMenuDefaults
2526
import androidx.compose.material3.Icon
2627
import androidx.compose.material3.IconButton
2728
import androidx.compose.material3.MaterialTheme
28-
import androidx.compose.material3.MenuAnchorType
2929
import androidx.compose.material3.OutlinedTextField
3030
import androidx.compose.material3.Text
3131
import androidx.compose.runtime.Composable
@@ -43,19 +43,20 @@ import androidx.compose.ui.res.painterResource
4343
import androidx.compose.ui.semantics.error
4444
import androidx.compose.ui.semantics.semantics
4545
import androidx.compose.ui.text.AnnotatedString
46+
import androidx.compose.ui.text.TextRange
47+
import androidx.compose.ui.text.input.TextFieldValue
4648
import androidx.core.graphics.drawable.toBitmap
4749
import com.google.android.fhir.datacapture.R
4850
import com.google.android.fhir.datacapture.views.factories.DropDownAnswerOption
4951

5052
@OptIn(ExperimentalMaterial3Api::class)
5153
@Composable
52-
internal fun ExposedDropDownMenuBoxItem(
54+
internal fun DropDownItem(
5355
modifier: Modifier,
5456
enabled: Boolean,
5557
labelText: AnnotatedString? = null,
5658
supportingText: String? = null,
5759
isError: Boolean = false,
58-
showClearIcon: Boolean = false,
5960
selectedOption: DropDownAnswerOption? = null,
6061
options: List<DropDownAnswerOption>,
6162
onDropDownAnswerOptionSelected: (DropDownAnswerOption?) -> Unit,
@@ -84,7 +85,7 @@ internal fun ExposedDropDownMenuBoxItem(
8485
Modifier.fillMaxWidth()
8586
.testTag(DROP_DOWN_TEXT_FIELD_TAG)
8687
.semantics { if (isError) error(supportingText ?: "") }
87-
.menuAnchor(MenuAnchorType.PrimaryNotEditable, enabled),
88+
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
8889
readOnly = true,
8990
enabled = enabled,
9091
minLines = 1,
@@ -101,6 +102,109 @@ internal fun ExposedDropDownMenuBoxItem(
101102
)
102103
}
103104
},
105+
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
106+
)
107+
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
108+
options.forEach { option ->
109+
DropDownAnswerMenuItem(enabled, option) {
110+
selectedDropDownAnswerOption = option
111+
expanded = false
112+
}
113+
}
114+
}
115+
}
116+
}
117+
118+
@OptIn(ExperimentalMaterial3Api::class)
119+
@Composable
120+
internal fun DropDownAnswerMenuItem(
121+
enabled: Boolean,
122+
answerOption: DropDownAnswerOption,
123+
onSelected: () -> Unit,
124+
) {
125+
DropdownMenuItem(
126+
modifier = Modifier.testTag(DROP_DOWN_ANSWER_MENU_ITEM_TAG),
127+
text = {
128+
Text(answerOption.answerOptionAnnotatedString(), style = MaterialTheme.typography.bodyLarge)
129+
},
130+
leadingIcon =
131+
answerOption.answerOptionImage?.let {
132+
{
133+
Icon(
134+
it.toBitmap().asImageBitmap(),
135+
contentDescription = answerOption.answerOptionString,
136+
)
137+
}
138+
},
139+
enabled = enabled,
140+
onClick = { onSelected() },
141+
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
142+
)
143+
}
144+
145+
@OptIn(ExperimentalMaterial3Api::class)
146+
@Composable
147+
internal fun AutoCompleteDropDownItem(
148+
modifier: Modifier,
149+
enabled: Boolean,
150+
labelText: AnnotatedString? = null,
151+
supportingText: String? = null,
152+
isError: Boolean = false,
153+
showClearIcon: Boolean = false,
154+
readOnly: Boolean = showClearIcon,
155+
selectedOption: DropDownAnswerOption? = null,
156+
options: List<DropDownAnswerOption>,
157+
onDropDownAnswerOptionSelected: (DropDownAnswerOption?) -> Unit,
158+
) {
159+
var expanded by remember { mutableStateOf(false) }
160+
var selectedDropDownAnswerOption by
161+
remember(selectedOption, options) { mutableStateOf(selectedOption) }
162+
var selectedOptionDisplay by
163+
remember(selectedDropDownAnswerOption) {
164+
val stringValue = selectedDropDownAnswerOption?.answerOptionString ?: ""
165+
mutableStateOf(TextFieldValue(stringValue, selection = TextRange(stringValue.length)))
166+
}
167+
val filteredOptions =
168+
remember(options, selectedOptionDisplay) {
169+
options.filter { it.answerOptionString.contains(selectedOptionDisplay.text, true) }
170+
}
171+
172+
LaunchedEffect(selectedDropDownAnswerOption) {
173+
onDropDownAnswerOptionSelected(selectedDropDownAnswerOption)
174+
}
175+
176+
ExposedDropdownMenuBox(
177+
modifier = modifier,
178+
expanded = expanded,
179+
onExpandedChange = { expanded = it },
180+
) {
181+
OutlinedTextField(
182+
value = selectedOptionDisplay,
183+
onValueChange = {
184+
selectedOptionDisplay = it
185+
if (!expanded) expanded = true
186+
},
187+
modifier =
188+
Modifier.fillMaxWidth()
189+
.testTag(DROP_DOWN_TEXT_FIELD_TAG)
190+
.semantics { if (isError) error(supportingText ?: "") }
191+
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, enabled),
192+
readOnly = readOnly,
193+
enabled = enabled,
194+
minLines = 1,
195+
isError = isError,
196+
label = { labelText?.let { Text(it) } },
197+
supportingText = { supportingText?.let { Text(it) } },
198+
leadingIcon =
199+
selectedDropDownAnswerOption?.answerOptionImage?.let {
200+
{
201+
Icon(
202+
it.toBitmap().asImageBitmap(),
203+
contentDescription = selectedDropDownAnswerOption!!.answerOptionString,
204+
modifier = Modifier.testTag(DROP_DOWN_TEXT_FIELD_LEADING_ICON_TAG),
205+
)
206+
}
207+
},
104208
trailingIcon = {
105209
Row(verticalAlignment = Alignment.CenterVertically) {
106210
if (showClearIcon) {
@@ -111,33 +215,26 @@ internal fun ExposedDropDownMenuBoxItem(
111215
Icon(painterResource(R.drawable.ic_clear), contentDescription = "clear")
112216
}
113217
}
114-
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
218+
ExposedDropdownMenuDefaults.TrailingIcon(
219+
expanded = expanded,
220+
modifier =
221+
Modifier.menuAnchor(
222+
ExposedDropdownMenuAnchorType.SecondaryEditable,
223+
enabled,
224+
),
225+
)
115226
}
116227
},
117228
)
118-
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
119-
options.forEach { option ->
120-
DropdownMenuItem(
121-
modifier = Modifier.testTag(DROP_DOWN_MENU_ITEM_TAG),
122-
text = {
123-
Text(option.answerOptionAnnotatedString(), style = MaterialTheme.typography.bodyLarge)
124-
},
125-
leadingIcon =
126-
option.answerOptionImage?.let {
127-
{
128-
Icon(
129-
it.toBitmap().asImageBitmap(),
130-
contentDescription = option.answerOptionString,
131-
)
132-
}
133-
},
134-
enabled = enabled,
135-
onClick = {
229+
230+
if (filteredOptions.isNotEmpty()) {
231+
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
232+
filteredOptions.forEach { option ->
233+
DropDownAnswerMenuItem(enabled, option) {
136234
selectedDropDownAnswerOption = option
137235
expanded = false
138-
},
139-
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
140-
)
236+
}
237+
}
141238
}
142239
}
143240
}
@@ -146,4 +243,4 @@ internal fun ExposedDropDownMenuBoxItem(
146243
const val CLEAR_TEXT_ICON_BUTTON_TAG = "clear_field_text"
147244
const val DROP_DOWN_TEXT_FIELD_TAG = "drop_down_text_field"
148245
const val DROP_DOWN_TEXT_FIELD_LEADING_ICON_TAG = "drop_down_text_field_leading_icon"
149-
const val DROP_DOWN_MENU_ITEM_TAG = "drop_down_list_menu_item"
246+
const val DROP_DOWN_ANSWER_MENU_ITEM_TAG = "drop_down_answer_list_menu_item"

0 commit comments

Comments
 (0)