Skip to content

Commit

Permalink
Expect androidx.compose.material3.DropdownMenu in common (#737)
Browse files Browse the repository at this point in the history
* Expect DropdownMenu in common

* Fix compatibility, use overload with properties

* Apply fixes from SkikoDropdownMenuPositionProvider

* Fix null safety

* Missing actual fun DropdownMenuItem

* Workaround for "Overload resolution ambiguity"

* Fix test

* Copy Rtl fix from material

* Revert DropdownMenuPositionProvider fixes

* Update Rtl test

* Fix DesktopMenuTest tests
  • Loading branch information
MatkovIvan authored and igordmn committed Jan 30, 2024
1 parent 3af4098 commit 6d0bc27
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 143 deletions.
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,58 +39,67 @@ 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 {

@get:Rule
val rule = createComposeRule()

val windowSize = IntSize(100, 100)
val anchorPosition = IntOffset(10, 10)
val anchorSize = IntSize(80, 20)

@Test
fun menu_positioning_vertical_underAnchor() {
val popupSize = IntSize(80, 70)
val windowSize = IntSize(200, 200)
val anchorBounds = IntRect(
offset = IntOffset(10, 100),
size = IntSize(80, 20)
)
val popupSize = IntSize(80, 50)

val position = SkikoDropdownMenuPositionProvider(
val position = DropdownMenuPositionProvider(
DpOffset.Zero,
Density(1f)
).calculatePosition(
IntRect(anchorPosition, anchorSize),
anchorBounds,
windowSize,
LayoutDirection.Ltr,
popupSize
)

assertThat(position).isEqualTo(IntOffset(10, 30))
assertThat(position).isEqualTo(
IntOffset(
x = anchorBounds.left,
y = anchorBounds.top - popupSize.height
)
)
}

// (RTL) Anchor right is beyond the right of the window, so align popup to the window right
@Test
fun menu_positioning_vertical_windowTop() {
val popupSize = IntSize(80, 100)

val position = SkikoDropdownMenuPositionProvider(
DpOffset.Zero,
Density(1f)
).calculatePosition(
IntRect(anchorPosition, anchorSize),
windowSize,
LayoutDirection.Ltr,
popupSize
)

assertThat(position).isEqualTo(IntOffset(10, 0))
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)
}

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

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

0 comments on commit 6d0bc27

Please sign in to comment.