android-jetpack-composeandroid-compose-textfieldandroid-jetpack-compose-material3android-compose-exposeddropdown

Editable Dynamic ExposedDropDownMenuBox in Jetpack Compose


I'm trying to get this drop down menu to be dynamic. When I type into the textfield, I expect the list to be updated (filtered for what the user types in) and the user can select from the filtered drop down list.

I've checked here, the sample code from the dev docs https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)

I found that material 3 drop down menu will block user input if it is in an expanded state. If a user starts to type and we say

onValueChange = { 
        selectedOptionText = it
        expanded = true
}

user is blocked from typing further because the ExposedDropdownMenu expanded blocks user input

How can I make a textfield dynamically open a drop down menu, and the list is updated based on user input? And the list items are selectable

To add some more context, I understand that DropdownMenu does not block user input if we say properties = PopupProperties(focusable = false)

DropdownMenu(
            expanded = expanded,
            onDismissRequest = { expanded = false },
            properties = PopupProperties(focusable = false)
        )

But the UI behavior is not the same. I'm looking for DropdownMenuBox behavior with DropdownMenu properties = PopupProperties(focusable = false)


Solution

  • This problem is solved by taking the code from the docs

    val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
    var expanded by remember { mutableStateOf(false) }
    var selectedOptionText by remember { mutableStateOf("") }
    ExposedDropdownMenuBox(
        expanded = expanded,
        onExpandedChange = { expanded = !expanded },
    ) {
        TextField(
            // The `menuAnchor` modifier must be passed to the text field for correctness.
            modifier = Modifier.menuAnchor(),
            value = selectedOptionText,
            onValueChange = { selectedOptionText = it },
            label = { Text("Label") },
            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
            colors = ExposedDropdownMenuDefaults.textFieldColors(),
        )
        // filter options based on text field value
        val filteringOptions = options.filter { it.contains(selectedOptionText, ignoreCase = true) }
        if (filteringOptions.isNotEmpty()) {
            ExposedDropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false },
            ) {
                filteringOptions.forEach { selectionOption ->
                    DropdownMenuItem(
                        text = { Text(selectionOption) },
                        onClick = {
                            selectedOptionText = selectionOption
                            expanded = false
                        },
                        contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
                    )
                }
            }
        }
    }
    

    And we replace the nested ExposedDropdownMenu with

    DropdownMenu(
        modifier = Modifier
            .background(Color.White)
            .exposedDropdownSize(true),
        properties = PopupProperties(focusable = false),
        expanded = expanded,
        onDismissRequest = { expanded = false },
    )
    

    the final code will look and behave as expected. When you type, the suggestions will follow dynamically and user input will not be blocked

    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun docs() {
        val options = listOf("Option 1", "comment", "Afghanistan", "Albania", "Algeria", "Andorra", "Egypt")
        var expanded by remember { mutableStateOf(false) }
        var selectedOptionText by remember { mutableStateOf("") }
        ExposedDropdownMenuBox(
            expanded = expanded,
            onExpandedChange = { expanded = !expanded },
        ) {
            TextField(
                // The `menuAnchor` modifier must be passed to the text field for correctness.
                modifier = Modifier.menuAnchor(),
                value = selectedOptionText,
                onValueChange = { selectedOptionText = it },
                label = { Text("Label") },
                trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
                colors = ExposedDropdownMenuDefaults.textFieldColors(
                    focusedContainerColor = Color.White,
                    unfocusedContainerColor = Color.White
                ),
            )
            // filter options based on text field value
            val filteringOptions = options.filter { it.contains(selectedOptionText, ignoreCase = true) }
            if (filteringOptions.isNotEmpty()) {
                DropdownMenu(
                    modifier = Modifier
                        .background(Color.White)
                        .exposedDropdownSize(true)
                    ,
                    properties = PopupProperties(focusable = false),
                    expanded = expanded,
                    onDismissRequest = { expanded = false },
                ) {
                    filteringOptions.forEach { selectionOption ->
                        DropdownMenuItem(
                            text = { Text(selectionOption) },
                            onClick = {
                                selectedOptionText = selectionOption
                                expanded = false
                            },
                            contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
                        )
                    }
                }
            }
        }
    }