From ec6f196e8829e70f6cfb240dc254500640090368 Mon Sep 17 00:00:00 2001 From: Siddharth Agarwal Date: Mon, 9 Sep 2024 19:26:39 +0530 Subject: [PATCH] Androapp 6035 mobile UI create dropdown menu component (#295) * Add data class for `MenuItem` component * Add `MenuItem` component * Add `MenuItemSnapshotTest` * Add `MenuItemTest` * Fix `MenuItem` Label and supporting text truncate * Fix snapshot test * add Enrolment menu to menu item showcase * Add shadow to enrollment menu --------- Co-authored-by: Siddharth Agarwal --- .../kotlin/org/hisp/dhis/common/App.kt | 2 + .../org/hisp/dhis/common/screens/Groups.kt | 1 + .../common/screens/others/MenuItemScreen.kt | 586 ++++++++++++++++++ .../ui/designsystem/MenuItemSnapshotTest.kt | 149 +++++ .../component/menuItem/MenuItem.kt | 300 +++++++++ .../component/menuItem/MenuItemTestTags.kt | 12 + .../mobile/ui/designsystem/theme/Shadow.kt | 6 +- .../ui/designsystem/component/MenuItemTest.kt | 128 ++++ ...enuItemSnapshotTest_launchMenuItemTest.png | Bin 0 -> 24529 bytes 9 files changed, 1181 insertions(+), 3 deletions(-) create mode 100644 common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt create mode 100644 designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt create mode 100644 designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt create mode 100644 designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index 66c039502..4f37602d7 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -23,6 +23,7 @@ import org.hisp.dhis.common.screens.others.BadgesScreen import org.hisp.dhis.common.screens.others.ChipsScreen import org.hisp.dhis.common.screens.others.IndicatorScreen import org.hisp.dhis.common.screens.others.LegendScreen +import org.hisp.dhis.common.screens.others.MenuItemScreen import org.hisp.dhis.common.screens.others.MetadataAvatarScreen import org.hisp.dhis.common.screens.others.NavigationBarScreen import org.hisp.dhis.common.screens.others.ProgressScreen @@ -101,6 +102,7 @@ fun Main( Groups.TAGS -> TagsScreen() Groups.SEARCH_BAR -> SearchBarScreen() Groups.NAVIGATION_BAR -> NavigationBarScreen() + Groups.MENU -> MenuItemScreen() Groups.NO_GROUP_SELECTED -> NoComponentSelectedScreen() Groups.TOP_BAR -> TopBarScreen() } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt index 821607631..703580911 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/Groups.kt @@ -19,5 +19,6 @@ enum class Groups(val label: String) { PARAMETER_SELECTOR("Parameter selector"), NAVIGATION_BAR("Navigation Bar"), TOP_BAR("Top Bar"), + MENU("Menu"), NO_GROUP_SELECTED("No group selected"), } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt new file mode 100644 index 000000000..bc27b2cfb --- /dev/null +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/others/MenuItemScreen.kt @@ -0,0 +1,586 @@ +package org.hisp.dhis.common.screens.others + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.automirrored.outlined.Assignment +import androidx.compose.material.icons.automirrored.outlined.HelpOutline +import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.DeleteForever +import androidx.compose.material.icons.outlined.DeleteOutline +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material.icons.outlined.Flag +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.outlined.Sync +import androidx.compose.material.icons.outlined.Workspaces +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import org.hisp.dhis.common.screens.Groups +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemState +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.hisp.dhis.mobile.ui.designsystem.theme.Radius +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import org.hisp.dhis.mobile.ui.designsystem.theme.dropShadow + +@Composable +fun MenuItemScreen() { + ColumnScreenContainer(Groups.MENU.label) { + ColumnComponentContainer( + "Enrollment dashboard menu", + ) { + var menuItems by remember { + mutableStateOf( + listOf( + MenuItemData( + label = "Refresh this record", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Sync), + ), + MenuItemData( + label = "Mark for follow-up", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Flag), + ), + MenuItemData( + label = "Group by stage", + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Workspaces), + ), + MenuItemData( + label = "Show help", + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.HelpOutline), + ), + MenuItemData( + label = "More enrollments", + leadingElement = MenuLeadingElement.Icon(icon = Icons.AutoMirrored.Outlined.Assignment), + ), + MenuItemData( + label = "Share", + supportingText = "Using QR code", + showDivider = true, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.Share), + ), + MenuItemData( + label = "Complete", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.CheckCircle, + defaultTintColor = SurfaceColor.CustomGreen, + selectedTintColor = SurfaceColor.CustomGreen, + ), + ), + MenuItemData( + label = "Deactivate", + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Cancel, + defaultTintColor = TextColor.OnDisabledSurface, + selectedTintColor = TextColor.OnDisabledSurface, + ), + ), + MenuItemData( + label = "Remove from [program]", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteOutline), + ), + MenuItemData( + label = "Delete [TEI Type]", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon(icon = Icons.Outlined.DeleteForever), + ), + ), + ) + } + + Column( + modifier = Modifier + .dropShadow( + shape = RoundedCornerShape(Radius.XS), + blur = Spacing.Spacing2, + spread = Spacing.Spacing0, + color = Color(0x4D007DEB), + offsetY = Spacing.Spacing1, + ) + .width(270.dp) + .background(SurfaceColor.ContainerLow) + .padding(vertical = Spacing.Spacing8), + ) { + menuItems.forEachIndexed { index, menuItemData -> + MenuItem( + menuItemData = menuItemData, + ) { + menuItems = menuItems.mapIndexed { i, item -> + if (i == index) { + item.copy(state = MenuItemState.SELECTED) + } else { + item.copy(state = MenuItemState.ENABLED) + } + } + } + } + } + } + + ColumnComponentContainer("Menu list item") { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + } + } + + ColumnComponentContainer("Menu item with leading element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Leading Element", + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + state = MenuItemState.DISABLED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + state = MenuItemState.DISABLED, + ), + ) {} + } + + ColumnComponentContainer("Menu item with divider") { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + showDivider = true, + ), + ) {} + } + + ColumnComponentContainer("Menu item with trailing element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Trailing Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Trailing Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Text Trailing Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Trailing Element", + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Text Trailing Element", + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Icon Trailing Element", + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Text Trailing Element", + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + } + + ColumnComponentContainer("Alert Menu item with leading element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Leading Element", + style = MenuItemStyle.ALERT, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + style = MenuItemStyle.ALERT, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + style = MenuItemStyle.ALERT, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Indent Leading Element", + leadingElement = MenuLeadingElement.Indent, + style = MenuItemStyle.ALERT, + state = MenuItemState.DISABLED, + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Diasbled Icon Leading Element", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + style = MenuItemStyle.ALERT, + state = MenuItemState.DISABLED, + ), + ) {} + } + + ColumnComponentContainer("Alert Menu item with divider") { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + showDivider = true, + style = MenuItemStyle.ALERT, + ), + ) {} + } + + ColumnComponentContainer("Alert Menu item with trailing element variations") { + MenuItem( + menuItemData = MenuItemData( + label = "No Trailing Element", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Icon Trailing Element", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Text Trailing Element", + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Icon Trailing Element", + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Selected Text Trailing Element", + style = MenuItemStyle.ALERT, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Icon Trailing Element", + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + menuItemData = MenuItemData( + label = "Disabled Text Trailing Element", + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Check, + ), + trailingElement = MenuTrailingElement.Text( + text = "⌘C", + ), + ), + ) {} + } + } +} diff --git a/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt new file mode 100644 index 000000000..badb7d95c --- /dev/null +++ b/designsystem/src/androidUnitTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/MenuItemSnapshotTest.kt @@ -0,0 +1,149 @@ +package org.hisp.dhis.mobile.ui.designsystem + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowRight +import androidx.compose.material.icons.outlined.Done +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnComponentContainer +import org.hisp.dhis.mobile.ui.designsystem.component.ColumnScreenContainer +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemState +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemStyle +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.junit.Rule +import org.junit.Test + +class MenuItemSnapshotTest { + @get:Rule + val paparazzi = paparazzi() + + @Test + fun launchMenuItemTest() { + paparazzi.snapshot { + ColumnScreenContainer("Menu Item") { + ColumnComponentContainer("Menu item") { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.SELECTED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + + Row( + horizontalArrangement = Arrangement.spacedBy(20.dp), + ) { + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + + MenuItem( + modifier = Modifier.weight(1f), + menuItemData = MenuItemData( + label = "Label", + supportingText = "Support Text", + showDivider = true, + state = MenuItemState.DISABLED, + style = MenuItemStyle.ALERT, + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + trailingElement = MenuTrailingElement.Icon( + icon = Icons.AutoMirrored.Outlined.ArrowRight, + ), + ), + ) {} + } + } + } + } + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt new file mode 100644 index 000000000..a07976bd9 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItem.kt @@ -0,0 +1,300 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.menuItem + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextOverflow +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_CONTAINER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_DIVIDER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_INDENT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.theme.Border +import org.hisp.dhis.mobile.ui.designsystem.theme.Outline +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +import org.hisp.dhis.mobile.ui.designsystem.theme.hoverPointerIcon + +/** + * DHIS2 [MenuItem] Used for dropdown menu. + * @param modifier: allows a modifier to be passed externally. + * @param menuItemData: manages the [MenuItemData] + * @param onItemClick: callback to when menu item is clicked. + */ +@Composable +fun MenuItem( + modifier: Modifier = Modifier, + menuItemData: MenuItemData, + onItemClick: () -> Unit, +) { + val itemContainerBackground = when (menuItemData.state) { + MenuItemState.SELECTED -> { + if (menuItemData.style == MenuItemStyle.ALERT) { + SurfaceColor.ErrorContainer + } else { + SurfaceColor.Container + } + } + + else -> Color.Transparent + } + + Column( + modifier = modifier.testTag(MENU_ITEM_CONTAINER), + ) { + Row( + modifier = Modifier + .background(itemContainerBackground) + .alpha(if (menuItemData.state != MenuItemState.DISABLED) 1f else 0.38f) + .clickable( + enabled = menuItemData.state != MenuItemState.DISABLED, + onClick = { + onItemClick.invoke() + }, + ) + .hoverPointerIcon(menuItemData.state != MenuItemState.DISABLED) + .height(Spacing.Spacing48) + .padding(horizontal = Spacing.Spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + MenuItemLeadingElement( + leadingElement = menuItemData.leadingElement, + style = menuItemData.style, + state = menuItemData.state, + ) + Column( + modifier = Modifier.weight(1f), + ) { + Text( + maxLines = if (!menuItemData.supportingText.isNullOrEmpty()) 1 else 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(MENU_ITEM_TEXT), + style = MaterialTheme.typography.bodyLarge, + color = if (menuItemData.style == MenuItemStyle.ALERT) SurfaceColor.Error else TextColor.OnSurface, + text = menuItemData.label, + ) + if (!menuItemData.supportingText.isNullOrEmpty()) { + Text( + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(MENU_ITEM_SUPPORTING_TEXT), + style = MaterialTheme.typography.bodyMedium, + color = if (menuItemData.style == MenuItemStyle.ALERT) TextColor.OnErrorContainer else TextColor.OnSurfaceVariant, + text = menuItemData.supportingText, + ) + } + } + + MenuItemTrailingElement( + trailingElement = menuItemData.trailingElement, + state = menuItemData.state, + ) + } + if (menuItemData.showDivider) { + HorizontalDivider( + modifier = modifier + .testTag(MENU_ITEM_DIVIDER) + .padding(vertical = Spacing.Spacing8), + thickness = Border.Thin, + color = Outline.Medium, + ) + } + } +} + +@Composable +private fun MenuItemLeadingElement( + leadingElement: MenuLeadingElement? = null, + style: MenuItemStyle, + state: MenuItemState, +) { + when (leadingElement) { + is MenuLeadingElement.Indent -> { + Box( + modifier = Modifier + .testTag(MENU_ITEM_LEADING_INDENT) + .padding(end = Spacing.Spacing12) + .size(Spacing.Spacing24), + ) + } + + is MenuLeadingElement.Icon -> { + val iconTint = when (state) { + MenuItemState.SELECTED -> { + if (style == MenuItemStyle.ALERT) { + leadingElement.selectedErrorTintColor + } else { + leadingElement.selectedTintColor + } + } + + else -> if (style == MenuItemStyle.ALERT) { + leadingElement.defaultErrorTintColor + } else { + leadingElement.defaultTintColor + } + } + Icon( + imageVector = leadingElement.icon, + modifier = Modifier + .testTag(MENU_ITEM_LEADING_ICON) + .padding(end = Spacing.Spacing12) + .size(Spacing.Spacing24), + contentDescription = null, + tint = iconTint, + ) + } + + else -> {} + } +} + +@Composable +private fun MenuItemTrailingElement( + trailingElement: MenuTrailingElement? = null, + state: MenuItemState, +) { + when (trailingElement) { + is MenuTrailingElement.Icon -> { + Icon( + modifier = Modifier + .testTag(MENU_ITEM_TRAILING_ICON) + .padding(start = Spacing.Spacing12) + .size(Spacing.Spacing24), + imageVector = trailingElement.icon, + contentDescription = null, + tint = if (state == MenuItemState.SELECTED) trailingElement.selectedTintColor else trailingElement.defaultTintColor, + ) + } + + is MenuTrailingElement.Text -> { + Text( + modifier = Modifier + .testTag(MENU_ITEM_TRAILING_TEXT) + .padding(start = Spacing.Spacing12), + style = MaterialTheme.typography.bodyLarge, + color = if (state == MenuItemState.SELECTED) TextColor.OnSurface else TextColor.OnSurfaceVariant, + text = trailingElement.text, + ) + } + + else -> {} + } +} + +/** + * DHIS2 [MenuItemData], + * class to control the [MenuItem] + * @param label: controls the text to be shown. + * @param state: controls the [MenuItem] state. + * @param style: controls the [MenuItem] style. + * @param leadingElement: controls the [MenuLeadingElement]. + * @param trailingElement: controls the [MenuTrailingElement]. + * @param supportingText: controls the supporting text to be shown. + * @param showDivider: controls whether a divider should be shown. + */ +data class MenuItemData( + val label: String, + val state: MenuItemState = MenuItemState.ENABLED, + val style: MenuItemStyle = MenuItemStyle.DEFAULT, + val leadingElement: MenuLeadingElement? = null, + val trailingElement: MenuTrailingElement? = null, + val supportingText: String? = null, + val showDivider: Boolean = false, +) + +/** + * DHIS2 MenuItemState, + * enum class to control the [MenuItem] state + */ +enum class MenuItemState { + ENABLED, + SELECTED, + DISABLED, +} + +/** + * DHIS2 MenuItemStyle, + * enum class to control the [MenuItem] style + */ +enum class MenuItemStyle { + DEFAULT, + ALERT, +} + +/** + * DHIS2 [MenuLeadingElement], + * class to control the [MenuItem] leading element + */ +sealed class MenuLeadingElement { + /** + * DHIS2 [Indent], + * class to control the [MenuLeadingElement] trailing element indent. + */ + data object Indent : MenuLeadingElement() + + /** + * DHIS2 [Icon], + * class to control the [MenuLeadingElement] trailing element icon. + * @param icon: controls the icon to be shown. + * @param defaultErrorTintColor: controls the error style tint color. + * @param defaultTintColor: controls the default tint color. + * @param selectedErrorTintColor: controls the error style tint color when selected. + * @param selectedTintColor: controls the tint color when selected. + */ + data class Icon( + val icon: ImageVector, + val defaultErrorTintColor: Color = SurfaceColor.Error, + val defaultTintColor: Color = SurfaceColor.Primary, + val selectedErrorTintColor: Color = TextColor.OnErrorContainer, + val selectedTintColor: Color = TextColor.OnPrimaryContainer, + ) : MenuLeadingElement() +} + +/** + * DHIS2 [MenuTrailingElement], + * class to control the [MenuItem] trailing element + */ +sealed class MenuTrailingElement { + /** + * DHIS2 [Icon], + * class to control the [MenuTrailingElement] trailing element icon. + * @param icon: controls the icon to be shown. + * @param defaultTintColor: controls the default tint color. + * @param selectedTintColor: controls the tint color when selected. + */ + data class Icon( + val icon: ImageVector, + val defaultTintColor: Color = TextColor.OnSurfaceVariant, + val selectedTintColor: Color = TextColor.OnSurface, + ) : MenuTrailingElement() + + /** + * DHIS2 [Text], + * class to control the [MenuTrailingElement] trailing element text. + * @param text: controls the text to be shown. + */ + data class Text( + val text: String, + ) : MenuTrailingElement() +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt new file mode 100644 index 000000000..43b04b84e --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/menuItem/MenuItemTestTags.kt @@ -0,0 +1,12 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.menuItem + +object MenuItemTestTags { + const val MENU_ITEM_CONTAINER = "MENU_ITEM_CONTAINER" + const val MENU_ITEM_TEXT = "MENU_ITEM_TEXT" + const val MENU_ITEM_SUPPORTING_TEXT = "MENU_ITEM_SUPPORTING_TEXT" + const val MENU_ITEM_DIVIDER = "MENU_ITEM_DIVIDER" + const val MENU_ITEM_LEADING_INDENT = "MENU_ITEM_LEADING_INDENT" + const val MENU_ITEM_LEADING_ICON = "MENU_ITEM_LEADING_ICON" + const val MENU_ITEM_TRAILING_ICON = "MENU_ITEM_TRAILING_ICON" + const val MENU_ITEM_TRAILING_TEXT = "MENU_ITEM_TRAILING_TEXT" +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt index 22affe405..14df826bc 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Shadow.kt @@ -98,7 +98,7 @@ internal fun Modifier.iconCardShadow( } }.padding(bottom = shadowRadius) -internal fun Modifier.dropShadow( +fun Modifier.dropShadow( shape: Shape, color: Color = SurfaceColor.Container, blur: Dp = 10.dp, @@ -112,10 +112,10 @@ internal fun Modifier.dropShadow( // Create a Paint object val paint = Paint() -// Apply specified color + // Apply specified color paint.color = color -// Check for valid blur radius + // Check for valid blur radius if (blur.toPx() > 0) { paint.asFrameworkPaint().apply { // Apply blur to the Paint diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt new file mode 100644 index 000000000..fdc17cd88 --- /dev/null +++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/MenuItemTest.kt @@ -0,0 +1,128 @@ +package org.hisp.dhis.mobile.ui.designsystem.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Done +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItem +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemData +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_CONTAINER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_DIVIDER +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_LEADING_INDENT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_SUPPORTING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_ICON +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuItemTestTags.MENU_ITEM_TRAILING_TEXT +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuLeadingElement +import org.hisp.dhis.mobile.ui.designsystem.component.menuItem.MenuTrailingElement +import org.junit.Rule +import org.junit.Test + +class MenuItemTest { + @get:Rule + val rule = createComposeRule() + + @Test + fun shouldDisplayMenuItemWithLabelCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_TEXT, true).assertExists() + } + + @Test + fun shouldDisplaySupportingTextCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + supportingText = "Supporting Text", + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_SUPPORTING_TEXT, true).assertExists() + } + + @Test + fun shouldDisplayDividerCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + showDivider = true, + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_DIVIDER, true).assertExists() + } + + @Test + fun shouldDisplayLeadingIndentCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + leadingElement = MenuLeadingElement.Indent, + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_LEADING_INDENT, true).assertExists() + } + + @Test + fun shouldDisplayLeadingIconCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + leadingElement = MenuLeadingElement.Icon( + icon = Icons.Outlined.Done, + ), + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_LEADING_ICON, true).assertExists() + } + + @Test + fun shouldDisplayTrailingIconCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + trailingElement = MenuTrailingElement.Icon( + icon = Icons.Outlined.Done, + ), + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_TRAILING_ICON, true).assertExists() + } + + @Test + fun shouldDisplayTrailingTextCorrectly() { + rule.setContent { + MenuItem( + menuItemData = MenuItemData( + label = "Menu Item", + trailingElement = MenuTrailingElement.Text( + text = "Trailing Text", + ), + ), + ) {} + } + rule.onNodeWithTag(MENU_ITEM_CONTAINER).assertExists() + rule.onNodeWithTag(MENU_ITEM_TRAILING_TEXT, true).assertExists() + } +} diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_MenuItemSnapshotTest_launchMenuItemTest.png new file mode 100644 index 0000000000000000000000000000000000000000..39faab564c544d23d0186ca81013100aa20d5967 GIT binary patch literal 24529 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 Kvi6+!o&NzP!7EV! literal 0 HcmV?d00001