Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DropdownMenu gets positioned on the opposite side when anchored outside the window #3129

Closed
hfeky opened this issue May 6, 2023 · 12 comments · Fixed by JetBrains/compose-multiplatform-core#555
Assignees
Labels
bug Something isn't working layers: popup/dialog

Comments

@hfeky
Copy link

hfeky commented May 6, 2023

Describe the bug

DropdownMenu gets positioned incorrectly for RTL layout direction when moved out of screen bounds.

Code Snippet

val horizontalScrollState = rememberScrollState()
var isMenuExpanded by remember { mutableStateOf(false) }

CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
    Box {
        Column(modifier = Modifier.horizontalScroll(horizontalScrollState)) {
            Row(modifier = Modifier.width(2000.dp)) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Button(
                        modifier = Modifier.width(160.dp),
                        onClick = {
                            isMenuExpanded = !isMenuExpanded
                        }
                    ) {
                        Text("Click me")
                    }
                    DropdownMenu(
                        modifier = Modifier.width(160.dp),
                        expanded = isMenuExpanded,
                        onDismissRequest = {
                            isMenuExpanded = false
                        },
                        focusable = false
                    ) {
                        repeat(10) {
                            DropdownMenuItem(
                                onClick = {
                                    isMenuExpanded = false
                                }
                            ) {
                                Text("Item $it")
                            }
                        }
                    }
                }
            }
        }
        HorizontalScrollbar(
            modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart),
            adapter = rememberScrollbarAdapter(
                scrollState = horizontalScrollState
            )
        )
    }
}

Affected platforms

All

Versions

  • Kotlin version*: 1.8.20
  • Compose Multiplatform version*: 1.5.0-dev1036
  • OS version(s)* (required for Desktop and iOS issues): macOS Ventura 13.1
  • OS architecture (x86 or arm64): arm64
  • JDK (for desktop issues): 11

To Reproduce

  1. Add a DropdownMenu inside a horizontal container with RTL layout direction.
  2. Scroll to the left.

Expected Behavior

The dropdown menu should remain fixed to the right end of the window when the layout direction is RTL.

Actual Behavior

The dropdown menu gets positioned to the left of the window when the layout direction is RTL.

Screenshot

Screen-Recording-2023-05-06-at-4.11.25-AM.mp4
@hfeky hfeky added bug Something isn't working submitted labels May 6, 2023
@m-sasha
Copy link
Member

m-sasha commented May 8, 2023

Can you post a complete working snippet? I'm having trouble reproducing it because when the menu is showing, the underlying UI is not receiving events, so it's not possible to scroll.

@hfeky
Copy link
Author

hfeky commented May 8, 2023

Can you post a complete working snippet? I'm having trouble reproducing it because when the menu is showing, the underlying UI is not receiving events, so it's not possible to scroll.

@m-sasha Yes, sure. I forgot to add a horizontal scrollbar and make the dropdown menu unfocusable. Please try the updated code snippet in the issue description.

@m-sasha
Copy link
Member

m-sasha commented May 8, 2023

Hmm. I changed the code a bit to move the dropdown to the left, but it seems to work fine in Compose 1.4.0, Compose 1.5.0-dev1036 and also in the very latest commit on our development branch:

Screen.Recording.2023-05-08.at.20.35.10.mp4

Does this exact code manifest the bug for you?

import androidx.compose.foundation.HorizontalScrollbar
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Button
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication{
    val horizontalScrollState = rememberScrollState()
    var isMenuExpanded by remember { mutableStateOf(false) }

    CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
        Box {
            Column(modifier = Modifier.horizontalScroll(horizontalScrollState)) {
                Row(modifier = Modifier.width(2000.dp)) {
                    Row(modifier = Modifier.padding(16.dp)) {
                        Spacer(Modifier.width(100.dp))
                        Button(
                            modifier = Modifier
                                .width(160.dp),
                            onClick = {
                                isMenuExpanded = !isMenuExpanded
                            }
                        ) {
                            Text("Click me")
                        }
                        DropdownMenu(
                            modifier = Modifier.width(160.dp),
                            expanded = isMenuExpanded,
                            onDismissRequest = {
                                isMenuExpanded = false
                            },
                            focusable = false
                        ) {
                            repeat(10) {
                                DropdownMenuItem(
                                    onClick = {
                                        isMenuExpanded = false
                                    }
                                ) {
                                    Text("Item $it")
                                }
                            }
                        }
                    }
                }
            }
            HorizontalScrollbar(
                modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart),
                adapter = rememberScrollbarAdapter(
                    scrollState = horizontalScrollState
                )
            )
        }
    }
}

@hfeky
Copy link
Author

hfeky commented May 8, 2023

@m-sasha Okay, interesting. I tried your code, andsingleWindowApplication made it work as shown in your recording, however using Window like the following would reproduce the issue as explained in the original description, so it seems like the issue is in using Window. This issue occurs on Compose 1.5.0-dev1036 and all older versions.

fun main() = application {
    Window(onCloseRequest = ::exitApplication) {
        val horizontalScrollState = rememberScrollState()
        var isMenuExpanded by remember { mutableStateOf(false) }

        CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
            Box {
                Column(modifier = Modifier.horizontalScroll(horizontalScrollState)) {
                    Row(modifier = Modifier.width(2000.dp)) {
                        Spacer(Modifier.width(100.dp))
                        Column(modifier = Modifier.padding(16.dp)) {
                            Button(
                                modifier = Modifier.width(160.dp),
                                onClick = {
                                    isMenuExpanded = !isMenuExpanded
                                }
                            ) {
                                Text("Click me")
                            }
                            DropdownMenu(
                                modifier = Modifier.width(160.dp),
                                expanded = isMenuExpanded,
                                onDismissRequest = {
                                    isMenuExpanded = false
                                },
                                focusable = false
                            ) {
                                repeat(10) {
                                    DropdownMenuItem(
                                        onClick = {
                                            isMenuExpanded = false
                                        }
                                    ) {
                                        Text("Item $it")
                                    }
                                }
                            }
                        }
                    }
                }
                HorizontalScrollbar(
                    modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart),
                    adapter = rememberScrollbarAdapter(
                        scrollState = horizontalScrollState
                    )
                )
            }
        }
    }
}
Screen-Recording-2023-05-08-at-9.08.18-PM.mp4

@m-sasha
Copy link
Member

m-sasha commented May 8, 2023

That code also works for me without the bug manifesting. Can you post all the code, including imports?

@hfeky
Copy link
Author

hfeky commented May 8, 2023

@m-sasha Okay, I discovered the issue. It turns out changing the default locale outside a Window like the following would reproduce the issue, so it seems like the issue is in changing the locale outside the Window, however changing the locale inside the Window works though, which is fine with me, but I would have never expected that. I missed that line in the previous message.

import androidx.compose.foundation.HorizontalScrollbar
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import java.util.*

fun main() = application {
    // Set arabic locale.
    Locale.setDefault(Locale("ar", "EG"))

    Window(onCloseRequest = ::exitApplication) {
        // Having it here instead works.
        // Locale.setDefault(Locale("ar", "EG"))

        val horizontalScrollState = rememberScrollState()
        var isMenuExpanded by remember { mutableStateOf(false) }

        CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
            Box {
                Column(modifier = Modifier.horizontalScroll(horizontalScrollState)) {
                    Row(modifier = Modifier.width(2000.dp)) {
                        Spacer(Modifier.width(100.dp))
                        Column(modifier = Modifier.padding(16.dp)) {
                            Button(
                                modifier = Modifier.width(160.dp),
                                onClick = {
                                    isMenuExpanded = !isMenuExpanded
                                }
                            ) {
                                Text("Click me")
                            }
                            DropdownMenu(
                                modifier = Modifier.width(160.dp),
                                expanded = isMenuExpanded,
                                onDismissRequest = {
                                    isMenuExpanded = false
                                },
                                focusable = false
                            ) {
                                repeat(10) {
                                    DropdownMenuItem(
                                        onClick = {
                                            isMenuExpanded = false
                                        }
                                    ) {
                                        Text("Item $it")
                                    }
                                }
                            }
                        }
                    }
                }
                HorizontalScrollbar(
                    modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart),
                    adapter = rememberScrollbarAdapter(
                        scrollState = horizontalScrollState
                    )
                )
            }
        }
    }
}

@m-sasha
Copy link
Member

m-sasha commented May 8, 2023

Ok, I have a reproducer, which amusingly doesn't require a locale change, nor LayoutDirection.Ltr, nor Window:

import androidx.compose.foundation.HorizontalScrollbar
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Button
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.material3.DropdownMenu
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication

fun main() = singleWindowApplication {
    val horizontalScrollState = rememberScrollState()
    var isMenuExpanded by remember { mutableStateOf(false) }
    Box {
        Column(modifier = Modifier.horizontalScroll(horizontalScrollState)) {
            Row(modifier = Modifier.width(2000.dp)) {
                Spacer(Modifier.width(100.dp))
                Column(modifier = Modifier.padding(16.dp)) {
                    Button(
                        modifier = Modifier.width(160.dp),
                        onClick = {
                            isMenuExpanded = !isMenuExpanded
                        }
                    ) {
                        Text("Click me")
                    }
                    DropdownMenu(
                        modifier = Modifier.width(160.dp),
                        expanded = isMenuExpanded,
                        onDismissRequest = {
                            isMenuExpanded = false
                        },
                        focusable = false
                    ) {
                        repeat(10) {
                            DropdownMenuItem(
                                onClick = {
                                    isMenuExpanded = false
                                }
                            ) {
                                Text("Item $it")
                            }
                        }
                    }
                }
            }
        }
        HorizontalScrollbar(
            modifier = Modifier.fillMaxWidth().align(Alignment.BottomStart),
            adapter = rememberScrollbarAdapter(
                scrollState = horizontalScrollState
            )
        )
    }
}
Screen.Recording.2023-05-08.at.21.27.44.mp4

With your permission, I will change the issue title.

@m-sasha m-sasha changed the title DropdownMenu gets positioned incorrectly for RTL layout direction when moved out of screen DropdownMenu gets positioned on the opposite side when anchored outside the window May 8, 2023
@hfeky
Copy link
Author

hfeky commented May 8, 2023

Nice bug find. 😮 Sure.

@m-sasha
Copy link
Member

m-sasha commented May 9, 2023

Note that there are actually two problems seen here:

  1. Incorrect positioning of the dropdown when the preferred positioning doesn't fit into the window.
  2. LocalLayoutDirection isn't propagated to the DesktopDropdownMenuPositionProvider.

@m-sasha
Copy link
Member

m-sasha commented May 11, 2023

Note that even with this fix, it will still not work correctly with RTL layout direction until "LocalLayoutDirection isn't propagated into Popup" is also fixed.

@hfeky
Copy link
Author

hfeky commented May 12, 2023

Thanks for the update!

@okushnikov
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working layers: popup/dialog
Projects
None yet
3 participants