From ababe0b3d347a7dd8ab87076975c2fd3b6dd07aa Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Fri, 31 Jan 2025 08:29:33 +0100 Subject: [PATCH] fix: [ANDROAPP-6744] correct bottom sheet issues and adapt for android 35 (#347) * fix: [ANDROAPP-6744] correct bottom sheet issues and adapt for android 35 * fix: [ANDROAPP-6744] update documentation * fix: [ANDROAPP-6744] Add BottomSheetShellDefaults class for default values, add new parameters to all internal components that use th BottomSheetShell. * fix: [ANDROAPP-6744] small modification to legend param order * fix: [ANDROAPP-6744] update documentation * fix: [ANDROAPP-6744] deprecate old component, create uiState class for bottom sheet, update usages * fix: [ANDROAPP-6744] update bottomsheet * fix: [ANDROAPP-6744] extract bottomSheetShellState to it's own class * fix: [ANDROAPP-6744] Add bottom window insets and padding to Input multiselection and modify legend data object --- .../actionInputs/InputBarCodeScreen.kt | 15 +- .../screens/actionInputs/InputQRCodeScreen.kt | 15 +- .../screens/bottomSheets/BottomSheetScreen.kt | 267 ++++++++++++------ .../bottomSheets/OrgTreeBottomSheetScreen.kt | 102 +++++++ .../toggleableInputs/InputDropDownScreen.kt | 28 ++ .../ui/designsystem/component/BottomSheet.kt | 183 ++++++++++++ .../designsystem/component/InputDropDown.kt | 42 ++- .../component/InputMultiSelection.kt | 29 +- .../designsystem/component/InputSignature.kt | 12 + .../ui/designsystem/component/Legend.kt | 31 +- .../designsystem/component/OrgBottomSheet.kt | 73 +++-- .../signature/SignatureBottomSheet.kt | 19 +- .../component/state/BottomSheetShellState.kt | 82 ++++++ .../mobile/ui/designsystem/theme/Shadow.kt | 8 +- .../mobile/ui/designsystem/theme/Spacing.kt | 1 + .../ui/designsystem/theme/SurfaceColor.kt | 1 + 16 files changed, 741 insertions(+), 167 deletions(-) create mode 100644 designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/BottomSheetShellState.kt diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputBarCodeScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputBarCodeScreen.kt index f22b74820..14031efe7 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputBarCodeScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputBarCodeScreen.kt @@ -26,6 +26,7 @@ import org.hisp.dhis.mobile.ui.designsystem.component.RowComponentContainer import org.hisp.dhis.mobile.ui.designsystem.component.SubTitle import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextData import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextState +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @@ -38,8 +39,15 @@ fun InputBarCodeScreen() { if (showEnabledBarCodeBottomSheet) { BottomSheetShell( + uiState = BottomSheetShellUIState( + title = provideStringResource("qr_code"), + ), modifier = Modifier.testTag("LEGEND_BOTTOM_SHEET"), - title = provideStringResource("qr_code"), + content = { + Row(horizontalArrangement = Arrangement.Center) { + BarcodeBlock(data = inputValue1.text) + } + }, icon = { Icon( imageVector = Icons.Outlined.Info, @@ -47,11 +55,6 @@ fun InputBarCodeScreen() { tint = SurfaceColor.Primary, ) }, - content = { - Row(horizontalArrangement = Arrangement.Center) { - BarcodeBlock(data = inputValue1.text) - } - }, buttonBlock = { ButtonCarousel( carouselButtonList = threeButtonCarousel, diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputQRCodeScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputQRCodeScreen.kt index 5e45c6199..e6289377a 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputQRCodeScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/actionInputs/InputQRCodeScreen.kt @@ -23,6 +23,7 @@ import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.QrCodeBlock import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextData import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextState +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @@ -34,8 +35,15 @@ fun InputQRCodeScreen() { if (showEnabledQRBottomSheet) { BottomSheetShell( + uiState = BottomSheetShellUIState( + title = provideStringResource("qr_code"), + ), modifier = Modifier.testTag("LEGEND_BOTTOM_SHEET"), - title = provideStringResource("qr_code"), + content = { + Row(horizontalArrangement = Arrangement.Center) { + QrCodeBlock(data = inputValue1.text) + } + }, icon = { Icon( imageVector = Icons.Outlined.Info, @@ -43,11 +51,6 @@ fun InputQRCodeScreen() { tint = SurfaceColor.Primary, ) }, - content = { - Row(horizontalArrangement = Arrangement.Center) { - QrCodeBlock(data = inputValue1.text) - } - }, buttonBlock = { ButtonCarousel( carouselButtonList = threeButtonCarousel, diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/BottomSheetScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/BottomSheetScreen.kt index 7018389bc..045e2f549 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/BottomSheetScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/BottomSheetScreen.kt @@ -36,6 +36,8 @@ import org.hisp.dhis.mobile.ui.designsystem.component.ColorStyle 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.LegendRange +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellDefaults +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor @@ -50,17 +52,13 @@ fun BottomSheetScreen() { var showBottomSheetWithSearchBar by rememberSaveable { mutableStateOf(false) } var showBottomSheetWithoutTitle by rememberSaveable { mutableStateOf(false) } var showBottomSheetWithoutContent by rememberSaveable { mutableStateOf(false) } + var showBottomSheetWithAndroid35Paddings by rememberSaveable { mutableStateOf(false) } if (showLegendBottomSheetShell) { BottomSheetShell( - title = "Legend name ", - icon = { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = "Button", - tint = SurfaceColor.Primary, - ) - }, + uiState = BottomSheetShellUIState( + title = "Legend name ", + ), content = { Column { LegendRange( @@ -68,14 +66,6 @@ fun BottomSheetScreen() { ) } }, - ) { - showLegendBottomSheetShell = false - } - } - if (showBottomSheetShellScrollableContent) { - val scrollState = rememberLazyListState() - BottomSheetShell( - title = "Legend name ", icon = { Icon( imageVector = Icons.Outlined.Info, @@ -83,28 +73,65 @@ fun BottomSheetScreen() { tint = SurfaceColor.Primary, ) }, - contentScrollState = scrollState, + ) { + showLegendBottomSheetShell = false + } + } + if (showBottomSheetShellScrollableContent) { + val scrollState = rememberLazyListState() + BottomSheetShell( + uiState = BottomSheetShellUIState( + title = "Legend name ", + ), content = { LazyColumn(state = scrollState) { items(longLegendList) { item -> Column { - Text(text = item.text, modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp)) + Text( + text = item.text, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp), + ) HorizontalDivider() } } } }, + contentScrollState = scrollState, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Button", + tint = SurfaceColor.Primary, + ) + }, ) { showBottomSheetShellScrollableContent = false } } if (showBottomSheetShellMaxExpansion) { BottomSheetShell( - title = "Legend name ", - subtitle = "Subtitle", - description = lorem + lorem, + uiState = BottomSheetShellUIState( + title = "Legend name ", + subtitle = "Subtitle", + description = lorem + lorem, + ), + content = { + Column { + LegendRange( + longLegendList, + ) + } + }, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Button", + tint = SurfaceColor.Primary, + ) + }, buttonBlock = { ButtonBlock( + modifier = Modifier.padding(BottomSheetShellDefaults.buttonBlockPaddings()), primaryButton = { Button( style = ButtonStyle.FILLED, @@ -124,6 +151,27 @@ fun BottomSheetScreen() { }, ) }, + ) { + showBottomSheetShellMaxExpansion = false + } + } + + if (showBottomSheetWithAndroid35Paddings) { + BottomSheetShell( + uiState = BottomSheetShellUIState( + title = "Legend name ", + bottomPadding = BottomSheetShellDefaults.lowerPadding(true), + subtitle = "Subtitle", + description = lorem + lorem, + ), + content = { + Column { + LegendRange( + longLegendList, + ) + } + }, + windowInsets = { BottomSheetShellDefaults.windowInsets(true) }, icon = { Icon( imageVector = Icons.Outlined.Info, @@ -131,12 +179,29 @@ fun BottomSheetScreen() { tint = SurfaceColor.Primary, ) }, - content = { - Column { - LegendRange( - longLegendList, - ) - } + buttonBlock = { + ButtonBlock( + modifier = Modifier.padding( + BottomSheetShellDefaults.buttonBlockPaddings(), + ), + primaryButton = { + Button( + style = ButtonStyle.FILLED, + icon = { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "Button", + ) + }, + enabled = true, + text = "Label", + onClick = { + showBottomSheetShellMaxExpansion = false + }, + modifier = Modifier.fillMaxWidth(), + ) + }, + ) }, ) { showBottomSheetShellMaxExpansion = false @@ -145,9 +210,25 @@ fun BottomSheetScreen() { if (showBottomSheetShellSingleButton) { BottomSheetShell( - title = "Legend name ", - subtitle = "Subtitle", - description = lorem, + uiState = BottomSheetShellUIState( + title = "Legend name ", + subtitle = "Subtitle", + description = lorem, + ), + content = { + Column { + LegendRange( + regularLegendList, + ) + } + }, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Button", + tint = SurfaceColor.Primary, + ) + }, buttonBlock = { ButtonBlock( primaryButton = { @@ -167,22 +248,9 @@ fun BottomSheetScreen() { modifier = Modifier.fillMaxWidth(), ) }, + modifier = Modifier.padding(BottomSheetShellDefaults.buttonBlockPaddings()), ) }, - icon = { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = "Button", - tint = SurfaceColor.Primary, - ) - }, - content = { - Column { - LegendRange( - regularLegendList, - ) - } - }, ) { showBottomSheetShellSingleButton = false } @@ -190,12 +258,28 @@ fun BottomSheetScreen() { if (showBottomSheetShellTwoButtons) { BottomSheetShell( - title = "Legend name ", - subtitle = "Subtitle", - description = lorem, + uiState = BottomSheetShellUIState( + title = "Legend name ", + subtitle = "Subtitle", + description = lorem, + ), + content = { + Column { + LegendRange( + regularLegendList, + ) + } + }, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Button", + tint = SurfaceColor.Primary, + ) + }, buttonBlock = { Row( - modifier = Modifier.padding(Spacing.Spacing24), + modifier = Modifier.padding(BottomSheetShellDefaults.buttonBlockPaddings()), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { @@ -234,20 +318,6 @@ fun BottomSheetScreen() { ) } }, - icon = { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = "Button", - tint = SurfaceColor.Primary, - ) - }, - content = { - Column { - LegendRange( - regularLegendList, - ) - } - }, ) { showBottomSheetShellTwoButtons = false } @@ -257,11 +327,28 @@ fun BottomSheetScreen() { var searchQuery by rememberSaveable { mutableStateOf("") } BottomSheetShell( - title = "Bottom Sheet with Search Bar", - subtitle = "Subtitle", - description = lorem, + uiState = BottomSheetShellUIState( + title = "Bottom Sheet with Search Bar", + subtitle = "Subtitle", + description = lorem, + ), + content = { + Column { + LegendRange( + regularLegendList, + ) + } + }, + icon = { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = "Button", + tint = SurfaceColor.Primary, + ) + }, buttonBlock = { ButtonBlock( + modifier = Modifier.padding(BottomSheetShellDefaults.buttonBlockPaddings()), primaryButton = { Button( style = ButtonStyle.OUTLINED, @@ -297,21 +384,6 @@ fun BottomSheetScreen() { }, ) }, - icon = { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = "Button", - tint = SurfaceColor.Primary, - ) - }, - content = { - Column { - LegendRange( - regularLegendList, - ) - } - }, - searchQuery = searchQuery, onSearchQueryChanged = { searchQuery = it }, onSearch = { searchQuery = it }, onDismiss = { @@ -324,6 +396,14 @@ fun BottomSheetScreen() { var searchQuery by rememberSaveable { mutableStateOf("") } BottomSheetShell( + uiState = BottomSheetShellUIState(), + content = { + Column { + LegendRange( + regularLegendList, + ) + } + }, icon = { Icon( imageVector = Icons.Outlined.Info, @@ -331,16 +411,8 @@ fun BottomSheetScreen() { tint = SurfaceColor.Primary, ) }, - searchQuery = searchQuery, onSearchQueryChanged = { searchQuery = it }, onSearch = { searchQuery = it }, - content = { - Column { - LegendRange( - regularLegendList, - ) - } - }, ) { showBottomSheetWithoutTitle = false } @@ -348,6 +420,11 @@ fun BottomSheetScreen() { if (showBottomSheetWithoutContent) { BottomSheetShell( + uiState = BottomSheetShellUIState( + showTopSectionDivider = true, + showBottomSectionDivider = false, + ), + content = null, icon = { Icon( imageVector = Icons.Outlined.Info, @@ -355,11 +432,10 @@ fun BottomSheetScreen() { tint = SurfaceColor.Primary, ) }, - title = "Delete item?", - description = "Item from this list will be deleted", buttonBlock = { Row( verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(BottomSheetShellDefaults.buttonBlockPaddings()), ) { Button( modifier = Modifier.weight(1f), @@ -382,7 +458,6 @@ fun BottomSheetScreen() { ) } }, - content = null, ) { showBottomSheetWithoutContent = false } @@ -438,6 +513,16 @@ fun BottomSheetScreen() { } } + ColumnComponentContainer("Bottom sheet shell with for devices with edge to edge enabled") { + Button( + enabled = true, + ButtonStyle.FILLED, + text = "Show Modal", + ) { + showBottomSheetWithAndroid35Paddings = !showBottomSheetWithAndroid35Paddings + } + } + ColumnComponentContainer("Bottom sheet shell with search bar") { Button( enabled = true, diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt index 348eb0df0..6aecc7dab 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/bottomSheets/OrgTreeBottomSheetScreen.kt @@ -247,6 +247,108 @@ private class OrgTreeItemsFakeRepo { isOpen = false, hasChildren = false, ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), + OrgTreeItem( + uid = "81", + label = "UHC TEST 2", + isOpen = false, + hasChildren = false, + ), ) private val childrenOrgItems = listOf( diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt index 932cc51db..43c1bcf4c 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt @@ -12,6 +12,7 @@ import org.hisp.dhis.mobile.ui.designsystem.component.InputDropDown import org.hisp.dhis.mobile.ui.designsystem.component.InputShellState import org.hisp.dhis.mobile.ui.designsystem.component.InputStyle import org.hisp.dhis.mobile.ui.designsystem.component.SupportingTextData +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellDefaults @Composable fun InputDropDownScreen() { @@ -115,6 +116,33 @@ fun InputDropDownScreen() { ) } + ColumnComponentContainer("Basic Input Dropdown for large set and devices with edge to edge enabled") { + var selectedItem4 by remember { mutableStateOf(null) } + var filteredItems by remember { mutableStateOf(options) } + InputDropDown( + title = "Label", + state = InputShellState.UNFOCUSED, + itemCount = filteredItems.size, + onSearchOption = { query -> + filteredItems = options.filter { it.label.contains(query) } + }, + fetchItem = { index -> filteredItems[index] }, + useDropDown = false, + onResetButtonClicked = { + selectedItem4 = null + }, + onItemSelected = { _, item -> + selectedItem4 = item + }, + selectedItem = selectedItem4, + loadOptions = { + /*no-op*/ + }, + bottomSheetLowerPadding = BottomSheetShellDefaults.lowerPadding(isEdgeToEdgeEnabled = true), + windowInsets = { BottomSheetShellDefaults.windowInsets(true) }, + ) + } + ColumnComponentContainer("Basic Input Dropdown with content ") { var selectedItem1 by remember { mutableStateOf(options[0]) } InputDropDown( diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt index 53b873ab1..6001ff9eb 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/BottomSheet.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding @@ -17,6 +18,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -41,6 +43,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import org.hisp.dhis.mobile.ui.designsystem.component.internal.Keyboard import org.hisp.dhis.mobile.ui.designsystem.component.internal.keyboardAsState +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.theme.Border import org.hisp.dhis.mobile.ui.designsystem.theme.InternalSizeValues import org.hisp.dhis.mobile.ui.designsystem.theme.Shape @@ -150,6 +153,7 @@ fun BottomSheetHeader( * @param scrollableContainerMaxHeight: Max size for scrollable content. */ @OptIn(ExperimentalMaterial3Api::class) +@Deprecated("Use the new BottomSheetShell with the new parameters") @Composable fun BottomSheetShell( content: @Composable (() -> Unit)?, @@ -314,3 +318,182 @@ fun BottomSheetShell( } } } + +/** + * DHIS2 BottomSheetShell. Wraps compose · [ModalBottomSheet]. + * desktop version to be implemented + * @param uiState UI data class of type [BottomSheetShellUIState] with all the values for the ui elements used in the component. + * @param windowInsets: The insets to use for the bottom sheet shell. + * @param icon: the icon to be shown. + * @param buttonBlock: Space for the lower buttons, use together with BottomSheetShellDefaults + * button block padding to ensure a correct style is displayed. + * @param content: to be shown under the header. + * @param contentScrollState: Pass custom scroll state when content is + * scrollable. For example, pass configure it when using `LazyColumn` to `Modifier.verticalScroll` + * for content. + * @param onSearchQueryChanged: Callback when search query is changed. + * @param onSearch: Callback when search action is triggered. + * @param onDismiss: gives access to the onDismiss event. + * @param modifier allows a modifier to be passed externally. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BottomSheetShell( + uiState: BottomSheetShellUIState, + modifier: Modifier = Modifier, + content: @Composable (() -> Unit)?, + windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + contentScrollState: ScrollableState = rememberScrollState(), + icon: @Composable (() -> Unit)? = null, + buttonBlock: @Composable (() -> Unit)? = null, + onSearchQueryChanged: ((String) -> Unit)? = null, + onSearch: ((String) -> Unit)? = null, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(true) + val scope = rememberCoroutineScope() + val keyboardState by keyboardAsState() + + var isKeyboardOpen by remember { mutableStateOf(false) } + val showHeader by remember { + derivedStateOf { + if (uiState.animateHeaderOnKeyboardAppearance) { + !uiState.title.isNullOrBlank() && !isKeyboardOpen + } else { + !uiState.title.isNullOrBlank() + } + } + } + + LaunchedEffect(keyboardState) { + isKeyboardOpen = keyboardState == Keyboard.Opened + } + + ModalBottomSheet( + modifier = modifier, + containerColor = Color.Transparent, + contentWindowInsets = windowInsets, + onDismissRequest = { + onDismiss() + }, + sheetState = sheetState, + dragHandle = { + Box( + modifier = Modifier.padding(top = Spacing.Spacing72), + ) { + BottomSheetIconButton( + icon = { + Icon( + imageVector = Icons.Outlined.Close, + contentDescription = "Button", + tint = SurfaceColor.SurfaceBright, + ) + }, + modifier = Modifier.padding(bottom = Spacing.Spacing4), + ) { + scope.launch { + onDismiss() + } + } + } + }, + ) { + val canScrollForward by derivedStateOf { contentScrollState.canScrollForward } + + Column( + modifier = Modifier.padding(bottom = Spacing0).background(SurfaceColor.SurfaceBright, Shape.ExtraLargeTop), + ) { + val scrollColumnShadow = if (canScrollForward) { + Modifier.innerShadow(blur = 32.dp) + } else { + Modifier + } + Column( + modifier = Modifier + .weight(1f, fill = false) + .padding(top = Spacing24), + ) { + val hasSearch = + uiState.searchQuery != null && onSearchQueryChanged != null && onSearch != null + AnimatedVisibility( + visible = showHeader, + ) { + BottomSheetHeader( + title = uiState.title!!, + subTitle = uiState.subtitle, + description = uiState.description, + icon = icon, + hasSearch = hasSearch, + headerTextAlignment = uiState.headerTextAlignment, + modifier = Modifier + .padding(vertical = Spacing0) + .align(Alignment.CenterHorizontally), + ) + } + + if (showHeader && hasSearch) { + Spacer(Modifier.requiredHeight(16.dp)) + } + + if (hasSearch) { + SearchBar( + modifier = Modifier.fillMaxWidth().padding(horizontal = Spacing24), + text = uiState.searchQuery!!, + onQueryChange = onSearchQueryChanged!!, + onSearch = onSearch!!, + ) + } + + if (showHeader || hasSearch) { + if (uiState.showTopSectionDivider) { + HorizontalDivider( + modifier = Modifier.fillMaxWidth() + .padding(top = Spacing24, start = Spacing24, end = Spacing24, bottom = Spacing0), + color = TextColor.OnDisabledSurface, + thickness = Border.Thin, + ) + } else { + Spacer(Modifier.requiredHeight(Spacing24)) + } + } + + content?.let { + val scrollModifier = if ((contentScrollState as? ScrollState) != null) { + Modifier.verticalScroll(contentScrollState) + } else { + Modifier + } + Column( + Modifier.then(scrollColumnShadow), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.requiredHeight(Spacing8)) + Column( + modifier = Modifier + .padding(horizontal = Spacing24) + .heightIn(uiState.scrollableContainerMinHeight, uiState.scrollableContainerMaxHeight) + .then(scrollModifier), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = spacedBy(Spacing8), + ) { + content.invoke() + Spacer(Modifier.requiredHeight(Spacing8)) + } + if (uiState.showBottomSectionDivider && !canScrollForward) { + HorizontalDivider( + modifier = Modifier.fillMaxWidth().padding(start = Spacing24, end = Spacing24, bottom = Spacing0, top = Spacing0), + color = TextColor.OnDisabledSurface, + thickness = Border.Thin, + ) + } + } + } + } + Spacer(Modifier.requiredHeight(Spacing24)) + buttonBlock?.let { + buttonBlock.invoke() + } + Spacer(Modifier.requiredHeight(uiState.bottomPadding)) + } + } +} diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt index 794aefb73..83c9a5c8d 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -13,6 +14,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults @@ -42,6 +44,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2SCustomTextStyles import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 @@ -77,6 +80,8 @@ private const val MAX_DROPDOWN_ITEMS_TO_SHOW = 50 * @param expanded: config whether the dropdown should be initially displayed. * @param useDropDown: use dropdown if true. Bottomsheet with search capability otherwise. * @param onDismiss: gives access to the onDismiss event. + * @param windowInsets: The insets to use for the bottom sheet shell. + * @param bottomSheetLowerPadding the lower padding to use for the bottom sheet * @param noResultsFoundString: text to be shown in pop up when no results are found. */ @OptIn(ExperimentalMaterial3Api::class) @@ -101,6 +106,8 @@ fun InputDropDown( useDropDown: Boolean = true, loadOptions: () -> Unit, onDismiss: () -> Unit = {}, + windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + bottomSheetLowerPadding: Dp = Spacing0, noResultsFoundString: String = provideStringResource("no_results_found"), searchToFindMoreString: String = provideStringResource("search_to_see_more"), ) { @@ -137,17 +144,25 @@ fun InputDropDown( val scrollState = rememberLazyListState() BottomSheetShell( + uiState = BottomSheetShellUIState( + showBottomSectionDivider = true, + showTopSectionDivider = true, + bottomPadding = bottomSheetLowerPadding, + title = title, + searchQuery = if (showSearchBar) { + searchQuery + } else { + null + }, + ), modifier = Modifier.testTag("INPUT_DROPDOWN_BOTTOM_SHEET"), - title = title, - contentScrollState = scrollState, content = { LazyColumn( modifier = Modifier .testTag("INPUT_DROPDOWN_BOTTOM_SHEET_ITEMS") .semantics { dropDownItemCount = itemCount - } - .padding(top = Spacing8), + }, state = scrollState, ) { when { @@ -189,23 +204,20 @@ fun InputDropDown( } } }, - onDismiss = { - showDropdown = false - onDismiss() - }, - searchQuery = if (showSearchBar) { - searchQuery - } else { - null - }, - onSearch = { + windowInsets = windowInsets, + contentScrollState = scrollState, + onSearchQueryChanged = { searchQuery = it onSearchOption(it) }, - onSearchQueryChanged = { + onSearch = { searchQuery = it onSearchOption(it) }, + onDismiss = { + showDropdown = false + onDismiss() + }, ) } } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputMultiSelection.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputMultiSelection.kt index 676ea0bb3..50ddf2c8a 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputMultiSelection.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputMultiSelection.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding @@ -16,6 +17,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.Cancel +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,9 +37,12 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor private const val INLINE_CHECKBOXES_MIN_REQ_ITEMS = 6 @@ -58,12 +64,14 @@ private const val MAX_CHECKBOXES_ITEMS_TO_SHOW = 50 * @param doneButtonText: text to be shown for accept button in pop up. * @param onClearItemSelection: callback for clear item selection. */ -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun InputMultiSelection( items: List, title: String, state: InputShellState, + windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + bottomSheetLowerPadding: Dp = Spacing0, supportingTextData: List?, legendData: LegendData?, isRequired: Boolean, @@ -223,6 +231,8 @@ fun InputMultiSelection( if (showMultiSelectBottomSheet) { MultiSelectBottomSheet( + windowInsets = windowInsets, + bottomSheetLowerPadding = bottomSheetLowerPadding, items = items, title = title, noResultsFoundString = noResultsFoundString, @@ -266,6 +276,7 @@ private fun SelectedItemChip( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MultiSelectBottomSheet( items: List, @@ -273,6 +284,8 @@ fun MultiSelectBottomSheet( noResultsFoundString: String, searchToFindMoreString: String, doneButtonText: String, + windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + bottomSheetLowerPadding: Dp = Spacing0, onItemsSelected: (List) -> Unit, onDismiss: () -> Unit, ) { @@ -285,8 +298,12 @@ fun MultiSelectBottomSheet( val itemsModified = remember { items.toMutableList() } BottomSheetShell( + uiState = BottomSheetShellUIState( + title = title, + bottomPadding = bottomSheetLowerPadding, + searchQuery = searchQuery, + ), modifier = Modifier.testTag("INPUT_MULTI_SELECT_BOTTOM_SHEET"), - title = title, content = { Column( modifier = Modifier @@ -330,10 +347,7 @@ fun MultiSelectBottomSheet( } } }, - onDismiss = onDismiss, - searchQuery = searchQuery, - onSearch = { searchQuery = it }, - onSearchQueryChanged = { searchQuery = it }, + windowInsets = windowInsets, buttonBlock = { Button( modifier = Modifier.fillMaxWidth(), @@ -354,6 +368,9 @@ fun MultiSelectBottomSheet( style = ButtonStyle.FILLED, ) }, + onSearchQueryChanged = { searchQuery = it }, + onSearch = { searchQuery = it }, + onDismiss = onDismiss, ) } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputSignature.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputSignature.kt index 03212003a..5ba92edb7 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputSignature.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputSignature.kt @@ -1,7 +1,10 @@ package org.hisp.dhis.mobile.ui.designsystem.component +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Draw +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -10,14 +13,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.Dp import org.hisp.dhis.mobile.ui.designsystem.component.internal.signature.SignatureBottomSheet import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 /** * DHIS2 Input signature. Wraps DHIS · [BasicInputImage]. * @param title: controls the text to be shown for the title. * @param state: manages the InputShell state. * @param inputStyle: manages the InputShell style. + * @param windowInsets: the insets for the bottom sheet shell. + * @param bottomSheetLowerPadding the padding for the bottom sheet shell. * @param supportingText: is a list of SupportingTextData that. * manages all the messages to be shown. * @param legendData: manages the legendComponent. @@ -33,11 +40,14 @@ import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource * @param onResetButtonClicked: callback to when reset button is clicked. * @param onSaveSignature: callback to when save button is clicked. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun InputSignature( title: String, state: InputShellState = InputShellState.UNFOCUSED, inputStyle: InputStyle = InputStyle.DataInputStyle(), + windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + bottomSheetLowerPadding: Dp = Spacing0, supportingText: List? = null, legendData: LegendData? = null, addSignatureBtnText: String = provideStringResource("add_signature"), @@ -86,6 +96,8 @@ fun InputSignature( onSaveSignature.invoke(it) showBottomSheet = false }, + windowInsets = windowInsets, + bottomSheetLowerPadding = bottomSheetLowerPadding, ) } } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Legend.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Legend.kt index 52d35c632..b464aa3f6 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Legend.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/Legend.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -15,6 +16,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.HelpOutline import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -32,18 +35,24 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.theme.Border import org.hisp.dhis.mobile.ui.designsystem.theme.InternalSizeValues import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.hisp.dhis.mobile.ui.designsystem.theme.hoverPointerIcon /** * DHIS2 Legend. * Used to display information on input value based on a range of values. - * @param legendData: data class with all parameters for component. - * @param modifier: optional modifier. + * @param legendData data class with all parameters for component. + * @param modifier optional modifier. + * @param windowInsets optional window insets to be used by the bottom sheet. + * @param bottomSheetLowerPadding optional bottom sheet lower padding. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun Legend( legendData: LegendData, @@ -117,8 +126,15 @@ fun Legend( if (showBottomSheetShell) { BottomSheetShell( + uiState = BottomSheetShellUIState( + title = legendData.title, + bottomPadding = legendData.bottomSheetLowerPadding, + ), modifier = Modifier.testTag("LEGEND_BOTTOM_SHEET"), - title = legendData.title, + content = { + legendData.popUpLegendDescriptionData?.let { LegendRange(it) } + }, + windowInsets = legendData.windowInsets, icon = { Icon( imageVector = Icons.Outlined.Info, @@ -126,9 +142,6 @@ fun Legend( tint = SurfaceColor.Primary, ) }, - content = { - legendData.popUpLegendDescriptionData?.let { LegendRange(it) } - }, ) { showBottomSheetShell = false } @@ -208,8 +221,12 @@ data class LegendDescriptionData( * @param popUpLegendDescriptionData list of [LegendDescriptionData] with information for the * legend range description pop up. */ -data class LegendData( +data class LegendData +@OptIn(ExperimentalMaterial3Api::class) +constructor( val color: Color, val title: String, val popUpLegendDescriptionData: List? = null, + val windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + val bottomSheetLowerPadding: Dp = Spacing0, ) diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt index 355d39d82..82e00ac7c 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/OrgBottomSheet.kt @@ -1,12 +1,12 @@ package org.hisp.dhis.mobile.ui.designsystem.component -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredWidth @@ -17,6 +17,8 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.ClearAll import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -41,12 +43,16 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellDefaults +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.resource.provideDHIS2Icon import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource import org.hisp.dhis.mobile.ui.designsystem.theme.DHIS2SCustomTextStyles import org.hisp.dhis.mobile.ui.designsystem.theme.InternalSizeValues import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor @@ -54,23 +60,26 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor * DHIS2 [OrgBottomSheet] component designed to be used * with Input Org Unit, wraps DHIS2 [BottomSheetShell]. * @param orgTreeItems list of [OrgTreeItem] with Org tree information - * @param title: Header. - * @param subtitle: optional subtitle. - * @param description: optional description. - * @param clearAllButtonText: text for clear all button. - * @param doneButtonText: text for accept button. - * @param doneButtonIcon: icon for accept button. - * @param noResultsFoundText: text for no results found. + * @param title Header. + * @param subtitle optional subtitle. + * @param description optional description. + * @param clearAllButtonText text for clear all button. + * @param doneButtonText text for accept button. + * @param doneButtonIcon icon for accept button. + * @param windowInsets The insets to use for the bottom sheet shell. + * @param bottomSheetLowerPadding padding for the bottom sheet. + * @param noResultsFoundText text for no results found. * @param headerTextAlignment [Alignment] for header text. - * @param icon: optional icon to be shown above the header . - * @param onSearch: access to the on search event. - * @param onDismiss: access to the on dismiss event. - * @param onItemSelected: access to the on item selected event. - * @param onItemClick: access to onItemClick event. - * @param onClearAll: access to the on clear all event. - * @param onDone: access to the on done event. + * @param icon optional icon to be shown above the header . + * @param onSearch access to the on search event. + * @param onDismiss access to the on dismiss event. + * @param onItemSelected access to the on item selected event. + * @param onItemClick access to onItemClick event. + * @param onClearAll access to the on clear all event. + * @param onDone access to the on done event. * @param modifier width and size of the barcode. */ +@OptIn(ExperimentalMaterial3Api::class) @Composable fun OrgBottomSheet( orgTreeItems: List, @@ -81,6 +90,8 @@ fun OrgBottomSheet( clearAllButtonText: String = provideStringResource("clear_all"), doneButtonText: String? = null, doneButtonIcon: ImageVector = Icons.Filled.Check, + windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + bottomSheetLowerPadding: Dp = Spacing0, noResultsFoundText: String = provideStringResource("no_results_found"), headerTextAlignment: TextAlign = TextAlign.Center, icon: @Composable (() -> Unit)? = null, @@ -94,21 +105,18 @@ fun OrgBottomSheet( var searchQuery by remember { mutableStateOf("") } var orgTreeHeight by remember { mutableStateOf(0) } val orgTreeHeightInDp = with(LocalDensity.current) { orgTreeHeight.toDp() } - BottomSheetShell( + uiState = BottomSheetShellUIState( + title = title, + subtitle = subtitle, + description = description, + headerTextAlignment = headerTextAlignment, + searchQuery = searchQuery, + scrollableContainerMaxHeight = maxOf(orgTreeHeightInDp, InternalSizeValues.Size386), + scrollableContainerMinHeight = InternalSizeValues.Size316, + bottomPadding = bottomSheetLowerPadding, + ), modifier = modifier, - title = title, - subtitle = subtitle, - description = description, - headerTextAlignment = headerTextAlignment, - icon = icon, - searchQuery = searchQuery, - onSearchQueryChanged = { query -> - searchQuery = query - onSearch?.invoke(searchQuery) - }, - onSearch = onSearch, - scrollableContainerMaxHeight = maxOf(orgTreeHeightInDp, InternalSizeValues.Size386), content = { OrgTreeList( orgTreeItems = orgTreeItems, @@ -125,10 +133,12 @@ fun OrgBottomSheet( }, ) }, + windowInsets = windowInsets, + icon = icon, buttonBlock = { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(Spacing.Spacing24), + modifier = Modifier.padding(BottomSheetShellDefaults.buttonBlockPaddings()), ) { if (onClearAll != null) { Button( @@ -163,6 +173,11 @@ fun OrgBottomSheet( ) } }, + onSearchQueryChanged = { query -> + searchQuery = query + onSearch?.invoke(searchQuery) + }, + onSearch = onSearch, onDismiss = onDismiss, ) } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/signature/SignatureBottomSheet.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/signature/SignatureBottomSheet.kt index 5edf63c60..b3311481d 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/signature/SignatureBottomSheet.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/internal/signature/SignatureBottomSheet.kt @@ -2,6 +2,7 @@ package org.hisp.dhis.mobile.ui.designsystem.component.internal.signature import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -9,6 +10,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.RestartAlt +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -21,12 +24,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell import org.hisp.dhis.mobile.ui.designsystem.component.Button import org.hisp.dhis.mobile.ui.designsystem.component.ButtonBlock import org.hisp.dhis.mobile.ui.designsystem.component.ButtonStyle import org.hisp.dhis.mobile.ui.designsystem.component.internal.dashedBorder +import org.hisp.dhis.mobile.ui.designsystem.component.state.BottomSheetShellUIState import org.hisp.dhis.mobile.ui.designsystem.resource.Signature import org.hisp.dhis.mobile.ui.designsystem.resource.SignatureCanvas import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource @@ -34,15 +39,19 @@ import org.hisp.dhis.mobile.ui.designsystem.theme.Border import org.hisp.dhis.mobile.ui.designsystem.theme.Color 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.Spacing.Spacing0 import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun SignatureBottomSheet( title: String, drawHereText: String = provideStringResource("draw_here"), resetButtonText: String = provideStringResource("reset"), doneButtonText: String = provideStringResource("done"), + bottomSheetLowerPadding: Dp = Spacing0, + windowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, onDismiss: () -> Unit, onSave: (ImageBitmap) -> Unit, ) { @@ -50,9 +59,12 @@ internal fun SignatureBottomSheet( var isSigning by rememberSaveable { mutableStateOf(false) } BottomSheetShell( + uiState = BottomSheetShellUIState( + title = title, + showTopSectionDivider = false, + bottomPadding = bottomSheetLowerPadding, + ), modifier = Modifier.testTag("INPUT_SIGNATURE_BOTTOM_SHEET"), - title = title, - showSectionDivider = false, content = { Box( modifier = Modifier @@ -92,7 +104,7 @@ internal fun SignatureBottomSheet( ) } }, - onDismiss = onDismiss, + windowInsets = windowInsets, buttonBlock = { ButtonBlock( primaryButton = { @@ -132,5 +144,6 @@ internal fun SignatureBottomSheet( }, ) }, + onDismiss = onDismiss, ) } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/BottomSheetShellState.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/BottomSheetShellState.kt new file mode 100644 index 000000000..5194ea676 --- /dev/null +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/state/BottomSheetShellState.kt @@ -0,0 +1,82 @@ +package org.hisp.dhis.mobile.ui.designsystem.component.state + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import org.hisp.dhis.mobile.ui.designsystem.theme.InternalSizeValues +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing0 +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing16 +import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing.Spacing24 + +/** + * Data class representing the UI state for the BottomSheetShell component. + * + * @property title The title to be displayed in the bottom sheet header. + * @property subtitle The subtitle to be displayed in the bottom sheet header. + * @property description The description to be displayed in the bottom sheet header. + * @property searchQuery The search query to be displayed in the search bar. + * @property showTopSectionDivider Whether to show the top section divider. + * @property showBottomSectionDivider Whether to show the bottom section divider. + * @property bottomPadding The lower padding for the bottom sheet shell. + * @property headerTextAlignment The alignment for the header text. + * @property scrollableContainerMinHeight The minimum height for the scrollable content container. + * @property scrollableContainerMaxHeight The maximum height for the scrollable content container. + * @property animateHeaderOnKeyboardAppearance Whether to animate the header when the keyboard appears. + */ + +data class BottomSheetShellUIState( + val title: String? = null, + val subtitle: String? = null, + val description: String? = null, + val searchQuery: String? = null, + val showTopSectionDivider: Boolean = true, + val showBottomSectionDivider: Boolean = true, + val bottomPadding: Dp = Spacing0, + val headerTextAlignment: TextAlign = TextAlign.Center, + val scrollableContainerMinHeight: Dp = Spacing0, + val scrollableContainerMaxHeight: Dp = InternalSizeValues.Size386, + val animateHeaderOnKeyboardAppearance: Boolean = true, +) + +/** + * Provides default values and configurations for the BottomSheet component. + */ +class BottomSheetShellDefaults { + + companion object { + /** + * Returns the default padding values for the button block in the BottomSheet. + * + * @return PaddingValues with top, bottom, start, and end padding. + */ + fun buttonBlockPaddings(): PaddingValues { + return PaddingValues(top = Spacing0, bottom = Spacing24, start = Spacing24, end = Spacing24) + } + + /** + * Returns the appropriate window insets for the BottomSheet based on whether edge-to-edge mode is enabled. + * + * @param isEdgeToEdgeEnabled Boolean indicating if edge-to-edge mode is enabled. + * @return WindowInsets with appropriate values based on the edge-to-edge mode. + */ + @Composable + @OptIn(ExperimentalMaterial3Api::class) + fun windowInsets(isEdgeToEdgeEnabled: Boolean): WindowInsets { + return if (isEdgeToEdgeEnabled) WindowInsets(0, 0, 0, 0) else { BottomSheetDefaults.windowInsets } + } + + /** + * Returns the appropriate lower padding for the BottomSheet based on whether edge-to-edge mode is enabled. + * + * @param isEdgeToEdgeEnabled Boolean indicating if edge-to-edge mode is enabled. + * @return a dp value based on the edge-to-edge mode. + */ + fun lowerPadding(isEdgeToEdgeEnabled: Boolean): Dp { + return if (isEdgeToEdgeEnabled) Spacing16 else Spacing0 + } + } +} 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 ea9bdf8d3..e6a4d24dd 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 @@ -89,13 +89,13 @@ internal fun Modifier.innerShadow( blur: Dp = 10.dp, ): Modifier = this.then( drawBehind { - val shadowSize = Size(size.width, 12.dp.toPx()) + val shadowSize = Size(size.width, 32.dp.toPx()) val shadowOutline = RectangleShape.createOutline(shadowSize, layoutDirection, this) // Create a Paint object val paint = Paint() // Apply specified color - paint.color = TextColor.OnSurfaceVariant.copy(alpha = 0.3f) + paint.color = SurfaceColor.CustomShadow.copy(alpha = 0.16f) // Check for valid blur radius if (blur.toPx() > 0) { @@ -109,8 +109,8 @@ internal fun Modifier.innerShadow( // Save the canvas state canvas.save() // Translate to specified offsets - canvas.translate(Spacing.Spacing0.toPx(), size.height - 12.dp.toPx()) - canvas.clipRect(0f, size.height - 12.dp.toPx(), size.width, size.height - 12.dp.toPx(), ClipOp.Difference) + canvas.translate(Spacing.Spacing0.toPx(), size.height) + canvas.clipRect(0f, 0f, size.width, size.height, ClipOp.Difference) // Draw the shadow canvas.drawOutline(shadowOutline, paint) // Restore the canvas state diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Spacing.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Spacing.kt index 391371e83..3f697c78b 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Spacing.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/Spacing.kt @@ -49,6 +49,7 @@ internal object InternalSizeValues { val Size64: Dp = 64.dp val Size120: Dp = 120.dp val Size300: Dp = 300.dp + val Size316: Dp = 316.dp val Size386: Dp = 386.dp val Size578: Dp = 578.dp val Size800: Dp = 800.dp diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/SurfaceColor.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/SurfaceColor.kt index bb6f325f6..623be1f89 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/SurfaceColor.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/theme/SurfaceColor.kt @@ -24,4 +24,5 @@ object SurfaceColor { val CustomYellow = Color.CustomYellow val CustomBrown = Color.CustomBrown val CustomGray = Color.CustomGray + val CustomShadow = Color.Blue900 }