Compose Component

DropdownMenuGroup

A composable for creating a visually distinct group within a DropdownMenuPopup.

DropdownMenuGroup social preview

GroupedMenuSample

@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun GroupedMenuSample() {
    val groupInteractionSource = remember { MutableInteractionSource() }
    var expanded by remember { mutableStateOf(false) }
    val groupLabels = listOf("Modification", "Navigation")
    val groupItemLabels = listOf(listOf("Edit", "Settings"), listOf("Home", "More Options"))
    val groupItemLeadingIcons =
        listOf(
            listOf(Icons.Outlined.Edit, Icons.Outlined.Settings),
            listOf(null, Icons.Outlined.Info),
        )
    val groupItemCheckedLeadingIcons =
        listOf(
            listOf(Icons.Filled.Edit, Icons.Filled.Settings),
            listOf(Icons.Filled.Check, Icons.Filled.Info),
        )
    val groupItemTrailingIcons: List<List<ImageVector?>> =
        listOf(listOf(null, null), listOf(Icons.Outlined.Home, Icons.Outlined.MoreVert))
    val groupItemCheckedTrailingIcons: List<List<ImageVector?>> =
        listOf(listOf(null, null), listOf(Icons.Filled.Home, Icons.Filled.MoreVert))
    val groupItemSupportingText: List<List<String?>> =
        listOf(listOf("Edit mode", null), listOf(null, "Opens menu"))
    val checked = remember {
        listOf(mutableStateListOf(false, false), mutableStateListOf(false, false))
    }
    Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
        // Icon button should have a tooltip associated with it for a11y.
        TooltipBox(
            positionProvider =
                TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
            tooltip = { PlainTooltip { Text("Localized description") } },
            state = rememberTooltipState(),
        ) {
            IconButton(onClick = { expanded = true }) {
                Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
            }
        }
        DropdownMenuPopup(expanded = expanded, onDismissRequest = { expanded = false }) {
            val groupCount = groupLabels.size
            groupLabels.fastForEachIndexed { groupIndex, label ->
                DropdownMenuGroup(
                    shapes = MenuDefaults.groupShape(groupIndex, groupCount),
                    interactionSource = groupInteractionSource,
                ) {
                    MenuDefaults.Label { Text(label) }
                    HorizontalDivider(
                        modifier = Modifier.padding(MenuDefaults.HorizontalDividerPadding)
                    )
                    val groupItemCount = groupItemLabels[groupIndex].size
                    groupItemLabels[groupIndex].fastForEachIndexed { itemIndex, itemLabel ->
                        DropdownMenuItem(
                            text = { Text(itemLabel) },
                            supportingText =
                                groupItemSupportingText[groupIndex][itemIndex]?.let { supportingText
                                    ->
                                    { Text(supportingText) }
                                },
                            shapes = MenuDefaults.itemShape(itemIndex, groupItemCount),
                            leadingIcon =
                                groupItemLeadingIcons[groupIndex][itemIndex]?.let { iconData ->
                                    {
                                        Icon(
                                            iconData,
                                            modifier = Modifier.size(MenuDefaults.LeadingIconSize),
                                            contentDescription = null,
                                        )
                                    }
                                },
                            checkedLeadingIcon = {
                                Icon(
                                    groupItemCheckedLeadingIcons[groupIndex][itemIndex],
                                    modifier = Modifier.size(MenuDefaults.LeadingIconSize),
                                    contentDescription = null,
                                )
                            },
                            trailingIcon =
                                if (checked[groupIndex][itemIndex]) {
                                    groupItemCheckedTrailingIcons[groupIndex][itemIndex]?.let {
                                        iconData ->
                                        {
                                            Icon(
                                                iconData,
                                                modifier =
                                                    Modifier.size(MenuDefaults.TrailingIconSize),
                                                contentDescription = null,
                                            )
                                        }
                                    }
                                } else {
                                    groupItemTrailingIcons[groupIndex][itemIndex]?.let { iconData ->
                                        {
                                            Icon(
                                                iconData,
                                                modifier =
                                                    Modifier.size(MenuDefaults.TrailingIconSize),
                                                contentDescription = null,
                                            )
                                        }
                                    }
                                },
                            checked = checked[groupIndex][itemIndex],
                            onCheckedChange = { checked[groupIndex][itemIndex] = it },
                        )
                    }
                }
                if (groupIndex != groupCount - 1) {
                    Spacer(Modifier.height(MenuDefaults.GroupSpacing))
                }
            }
            if (checked.last().last()) {
                DropdownMenuButtonGroup()
            }
        }
    }
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun MenuWithCascadingMenusSample() {
    val groupInteractionSource = remember { MutableInteractionSource() }
    var expanded by remember { mutableStateOf(false) }
    val groupItemLabels = listOf("Text", "Align", "Line spacing")
    val mainGroupItemLeadingIcons =
        listOf(
            Icons.Filled.FormatBold,
            Icons.AutoMirrored.Filled.FormatAlignLeft,
            Icons.Filled.FormatLineSpacing,
        )
    val submenus: List<@Composable (MutableInteractionSource) -> Unit> =
        listOf(
            { interactionSource -> TextSubmenu(interactionSource) },
            { interactionSource -> AlignSubmenu(interactionSource) },
            { interactionSource -> LineSpacingSubmenu(interactionSource) },
        )
    Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
        // Icon button should have a tooltip associated with it for a11y.
        TooltipBox(
            positionProvider =
                TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
            tooltip = { PlainTooltip { Text("Localized description") } },
            state = rememberTooltipState(),
        ) {
            IconButton(onClick = { expanded = true }) {
                Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
            }
        }
        DropdownMenuPopup(expanded = expanded, onDismissRequest = { expanded = false }) {
            DropdownMenuGroup(
                shapes = MenuDefaults.groupShape(0, 1),
                interactionSource = groupInteractionSource,
            ) {
                val groupItemCount = groupItemLabels.size
                groupItemLabels.fastForEachIndexed { itemIndex, label ->
                    Box {
                        val itemInteractionSource = remember { MutableInteractionSource() }
                        val itemHovered by itemInteractionSource.collectIsHoveredAsState()
                        var itemChecked by remember { mutableStateOf(false) }
                        DropdownMenuItem(
                            interactionSource = itemInteractionSource,
                            text = { Text(label) },
                            shape =
                                if (itemIndex == 0) MenuDefaults.leadingItemShape
                                else if (itemIndex == groupItemCount - 1)
                                    MenuDefaults.trailingItemShape
                                else MenuDefaults.middleItemShape,
                            leadingIcon = {
                                Icon(
                                    mainGroupItemLeadingIcons[itemIndex],
                                    modifier = Modifier.size(MenuDefaults.LeadingIconSize),
                                    contentDescription = null,
                                )
                            },
                            trailingIcon = {
                                Icon(
                                    Icons.AutoMirrored.Filled.ArrowRight,
                                    modifier = Modifier.size(MenuDefaults.TrailingIconSize),
                                    contentDescription = null,
                                )
                            },
                            onClick = { itemChecked = !itemChecked },
                        )
                        DropdownMenuPopup(
                            popupPositionProvider =
                                MenuDefaults.rememberDropdownMenuPopupPositionProvider(
                                    MenuAnchorPosition.End
                                ),
                            expanded = itemChecked || itemHovered,
                            onDismissRequest = { itemChecked = false },
                            properties = PopupProperties(focusable = false),
                        ) {
                            submenus[itemIndex](itemInteractionSource)
                        }
                    }
                }
            }
        }
    }
}

Last updated: