compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

OutlinedTextField doesn't produce PressInteraction.Press interactions on desktop and ios

Open Emt-tz opened this issue 1 year ago • 3 comments

I am building an expense app in compose multiplatform (1.5.0) but I have an issue with this

@Composable
fun ExposedDropdownMenu(
    state: MutableState<String>,
    items: List<String>,
    selected: String = items[0],
    onItemSelected: (String) -> Unit,
    colors: TextFieldColors = TextFieldDefaults.textFieldColors(
        focusedIndicatorColor = Color.Transparent,
        unfocusedIndicatorColor = Color.Transparent,
        disabledIndicatorColor = Color.Transparent
    ),
    textStyle: TextStyle = MaterialTheme.typography.body1.copy(
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp
    ),
) {
    var expanded by remember { mutableStateOf(false) }
    val interactionSource = remember { MutableInteractionSource() }
    LaunchedEffect(interactionSource) {
        interactionSource.interactions
            .filter { it is PressInteraction.Press }
            .collect {
                expanded = !expanded
            }
    }

    ExposedDropdownMenuStack(
        textField = {
            OutlinedTextField(
                value = if (state.value == "") selected else state.value,
                onValueChange = {
                    state.value = it
                },
                interactionSource = interactionSource,
                readOnly = true,
                colors = colors,
                singleLine = true,
                textStyle = textStyle,
                trailingIcon = {
                    val rotation by animateFloatAsState(if (expanded) 180F else 0F)
                    Icon(
                        rememberVectorPainter(Icons.Default.ArrowDropDown),
                        contentDescription = "Dropdown Arrow",
                        Modifier.rotate(rotation),
                    )
                },
                modifier = Modifier.fillMaxWidth()
            )
        },
        dropdownMenu = { boxWidth, itemHeight ->
            Box(
                Modifier
                    .width(boxWidth)
                    .wrapContentSize(Alignment.TopStart)
            ) {
                DropdownMenu(
                    expanded = expanded,
                    onDismissRequest = { expanded = false }
                ) {
                    items.forEach { item ->
                        DropdownMenuItem(
                            modifier = Modifier
                                .height(itemHeight)
                                .width(boxWidth),
                            onClick = {
                                expanded = false
                                state.value = item
                                onItemSelected(item)
                            }
                        ) {
                            Text(
                                text = item,
                                style = textStyle,
                                modifier = Modifier.fillMaxWidth()
                            )
                        }
                    }
                }
            }
        }
    )
}

@Composable
private fun ExposedDropdownMenuStack(
    textField: @Composable () -> Unit,
    dropdownMenu: @Composable (boxWidth: Dp, itemHeight: Dp) -> Unit
) {
    SubcomposeLayout { constraints ->
        val textFieldPlaceable =
            subcompose(ExposedDropdownMenuSlot.TextField, textField).first().measure(constraints)
        val dropdownPlaceable = subcompose(ExposedDropdownMenuSlot.Dropdown) {
            dropdownMenu(textFieldPlaceable.width.toDp(), textFieldPlaceable.height.toDp())
        }.first().measure(constraints)
        layout(textFieldPlaceable.width, textFieldPlaceable.height) {
            textFieldPlaceable.placeRelative(0, 0)
            dropdownPlaceable.placeRelative(0, textFieldPlaceable.height)
        }
    }
}

private enum class ExposedDropdownMenuSlot { TextField, Dropdown }

In Android, it's working fine but on iOS it does not work on clicking. But also I have trouble hiding the keyboard as items appear to be hidden under the keyboard in Android the manifest file I can adjust resize to that when the keyboard appears then it pushes content upwards.

@Composable
    fun DropDownField(
        label: String = "",
        onValueChange: (String) -> Unit,
        state: MutableState<String>,
        items: List<String>
    ) {
        ExposedDropdownMenu(
            state = state,
            items = items,
            selected = label,
            onItemSelected = onValueChange
        )
    }

Here is my list

CustomTextField.DropDownField(
            state = category,
            label = "Category",
            items = categories.keys.toList(),
            onValueChange = {
                category.value = it
            })

        categories[category.value]?.toList()?.let {
            CustomTextField.DropDownField(
                state = subcategory,
                label = "Sub-Category",
                items = it,
                onValueChange = {
                    subcategory.value = it
                })
        }

and here is the list

 val category = remember { mutableStateOf("") }
    val subcategory = remember { mutableStateOf("") }
    var amount by remember { mutableLongStateOf(0L) }
    var description by remember { mutableStateOf("") }

    // Map of categories to their subcategories
    val categories = mapOf(
        "Transport" to listOf("Fuel", "Public Transit", "Maintenance"),
        "Groceries" to listOf("Food", "Household Items"),
        "Entertainment" to listOf("Movies", "Concerts", "Games"),
    )

Emt-tz avatar Jan 02 '24 08:01 Emt-tz

Simplified a reproducer (https://github.com/eymar/repr_ios_subcompose_layout_click):

@Composable
internal fun App() = MaterialTheme {
    Column {
        Spacer(modifier = Modifier.height(200.dp))

        val interactionSource = remember { MutableInteractionSource() }
        LaunchedEffect(interactionSource) {
            interactionSource.interactions
                .collect { println("Interaction = $it") }
        }

        OutlinedTextField(
            value = "Try click me",
            onValueChange = {},
            interactionSource = interactionSource,
            readOnly = true,
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )
    }
}

On iOS and desktop it never prints androidx.compose.foundation.interaction.PressInteraction$Press, only androidx.compose.foundation.interaction.FocusInteraction$Focus.

But on android it prints both.


It means the issue is not with subcompose layout, but with OutlinedTextField.

You can try to change your code to not rely on PressInteraction.Press to workaround the issue for now.

eymar avatar Jan 02 '24 16:01 eymar

Note for our team: For the reproducer in mpp:demo have a look at https://github.com/JetBrains/compose-multiplatform-core/commit/3e390ddf9893f1360f85936f553b512ab34384f3#diff-d39ddc7e61fa5922223e673e2c9a3a82682d808f3394404b7250d302b3a25327R28

eymar avatar Jan 08 '24 09:01 eymar

Thank you Sir.

Emt-tz avatar Feb 06 '24 13:02 Emt-tz

Any updates on this issue?

natangr avatar Aug 05 '24 18:08 natangr