A composable for creating a visually distinct group within a DropdownMenuPopup.
@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)
}
}
}
}
}
}
}