JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.88k stars 1.15k forks source link

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

Open Emt-tz opened 8 months ago

Emt-tz commented 8 months ago

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"),
    )
eymar commented 8 months ago

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 commented 8 months ago

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

Emt-tz commented 7 months ago

Thank you Sir.

natangr commented 1 month ago

Any updates on this issue?

okushnikov commented 2 weeks ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.