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

Expect androidx.compose.material3.DropdownMenu in common #737

Merged
merged 11 commits into from
Aug 10, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ import androidx.compose.ui.window.PopupProperties
*/
@Suppress("ModifierParameter")
@Composable
fun DropdownMenu(
actual fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
modifier: Modifier,
offset: DpOffset,
properties: PopupProperties,
content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember { MutableTransitionState(false) }
Expand Down Expand Up @@ -135,16 +135,16 @@ fun DropdownMenu(
* [Interaction]s and customize the appearance / behavior of this menu item in different states.
*/
@Composable
fun DropdownMenuItem(
actual fun DropdownMenuItem(
text: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
colors: MenuItemColors = MenuDefaults.itemColors(),
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
modifier: Modifier,
leadingIcon: @Composable (() -> Unit)?,
trailingIcon: @Composable (() -> Unit)?,
enabled: Boolean,
colors: MenuItemColors,
contentPadding: PaddingValues,
interactionSource: MutableInteractionSource,
) {
DropdownMenuItemContent(
text = text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -38,13 +39,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.tokens.MenuTokens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
Expand All @@ -57,10 +52,100 @@ import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import kotlin.math.max
import kotlin.math.min


/**
* <a href="https://m3.material.io/components/menus/overview" class="external" target="_blank">Material Design dropdown menu</a>.
*
* Menus display a list of choices on a temporary surface. They appear when users interact with a
* button, action, or other control.
*
* ![Dropdown menu image](https://developer.android.com/images/reference/androidx/compose/material3/menu.png)
*
* A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
* to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
* that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
* space in a layout, as the menu is displayed in a separate window, on top of other content.
*
* The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
* content. Using [DropdownMenuItem]s will result in a menu that matches the Material
* specification for menus. Also note that the [content] is placed inside a scrollable [Column],
* so using a [LazyColumn] as the root layout inside [content] is unsupported.
*
* [onDismissRequest] will be called when the menu should close - for example when there is a
* tap outside the menu, or when the back key is pressed.
*
* [DropdownMenu] changes its positioning depending on the available space, always trying to be
* fully visible. It will try to expand horizontally, depending on layout direction, to the end of
* its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
* try to expand to the bottom of its parent, then from the top of its parent, and then screen
* top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
* the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
* be applied in the direction in which the menu will decide to expand.
*
* Example usage:
* @sample androidx.compose.material3.samples.MenuSample
*
* @param expanded whether the menu is expanded or not
* @param onDismissRequest called when the user requests to dismiss the menu, such as by tapping
* outside the menu's bounds
* @param offset [DpOffset] to be added to the position of the menu
*/
@Composable
expect fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
)

/**
* <a href="https://m3.material.io/components/menus/overview" class="external" target="_blank">Material Design dropdown menu</a> item.
*
* Menus display a list of choices on a temporary surface. They appear when users interact with a
* button, action, or other control.
*
* ![Dropdown menu image](https://developer.android.com/images/reference/androidx/compose/material3/menu.png)
*
* Example usage:
* @sample androidx.compose.material3.samples.MenuSample
*
* @param text text of the menu item
* @param onClick called when this menu item is clicked
* @param modifier the [Modifier] to be applied to this menu item
* @param leadingIcon optional leading icon to be displayed at the beginning of the item's text
* @param trailingIcon optional trailing icon to be displayed at the end of the item's text. This
* trailing icon slot can also accept [Text] to indicate a keyboard shortcut.
* @param enabled controls the enabled state of this menu item. When `false`, this component will
* not respond to user input, and it will appear visually disabled and disabled to accessibility
* services.
* @param colors [MenuItemColors] that will be used to resolve the colors used for this menu item in
* different states. See [MenuDefaults.itemColors].
* @param contentPadding the padding applied to the content of this menu item
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
* for this menu item. You can create and pass in your own `remember`ed instance to observe
* [Interaction]s and customize the appearance / behavior of this menu item in different states.
*/
@Composable
expect fun DropdownMenuItem(
text: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
colors: MenuItemColors = MenuDefaults.itemColors(),
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
)

@Suppress("ModifierParameter")
@Composable
internal fun DropdownMenuContent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@

package androidx.compose.material3

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material3.internal.keyEvent
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
import androidx.compose.ui.test.getBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performKeyPress
Expand All @@ -33,12 +39,14 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.size
import com.google.common.truth.Truth.assertThat
import org.junit.Assert
import org.junit.Rule
import org.junit.runners.JUnit4
import org.junit.runner.RunWith
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class DesktopMenuTest {
Expand All @@ -54,7 +62,7 @@ class DesktopMenuTest {
fun menu_positioning_vertical_underAnchor() {
val popupSize = IntSize(80, 70)

val position = SkikoDropdownMenuPositionProvider(
val position = DropdownMenuPositionProvider(
DpOffset.Zero,
Density(1f)
).calculatePosition(
Expand All @@ -71,7 +79,7 @@ class DesktopMenuTest {
fun menu_positioning_vertical_windowTop() {
val popupSize = IntSize(80, 100)

val position = SkikoDropdownMenuPositionProvider(
val position = DropdownMenuPositionProvider(
DpOffset.Zero,
Density(1f)
).calculatePosition(
Expand All @@ -84,7 +92,25 @@ class DesktopMenuTest {
assertThat(position).isEqualTo(IntOffset(10, 0))
}

@OptIn(ExperimentalComposeUiApi::class)
// (RTL) Anchor right is beyond the right of the window, so align popup to the window right
@Test
fun menu_positioning_rtl_windowRight_belowAnchor() {
rule.setContent {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
Box(Modifier.fillMaxSize().testTag("background")) {
Box(Modifier.offset(x = (-10).dp).size(50.dp)) {
DropdownMenu(true, onDismissRequest = {}) {
Box(Modifier.size(50.dp).testTag("box"))
}
}
}
}
}
val windowSize = rule.onNodeWithTag("background").getBoundsInRoot().size
rule.onNodeWithTag("box")
.assertLeftPositionInRootIsEqualTo(windowSize.width - 50.dp)
}

@Test
fun `pressing ESC button invokes onDismissRequest`() {
var dismissCount = 0
Expand Down Expand Up @@ -113,7 +139,6 @@ class DesktopMenuTest {
}
}

@OptIn(ExperimentalComposeUiApi::class)
@Test
fun `navigate DropDownMenu using arrows`() {
var item1Clicked = 0
Expand Down