PM-25003: Migrate bottom sheet to the UI module (#5751)

This commit is contained in:
David Perez
2025-08-19 15:58:03 -05:00
committed by GitHub
parent 070ef45087
commit 4a18e57cca
8 changed files with 15 additions and 15 deletions

View File

@@ -0,0 +1,111 @@
package com.bitwarden.ui.platform.components.bottomsheet
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.SemanticsPropertyKey
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
/**
* A reusable modal bottom sheet that applies provides a bottom sheet layout with the
* standard [BitwardenScaffold] and [BitwardenTopAppBar] and expected scrolling behavior with
* passed in [sheetContent]
*
* @param sheetTitle The title to display in the [BitwardenTopAppBar]
* @param onDismiss The action to perform when the bottom sheet is dismissed will also be performed
* when the "close" icon is clicked, caller must handle any desired animation or hiding of the
* bottom sheet. This will be invoked _after_ the sheet has been animated away.
* @param topBarActions Row of actions to add the top bar of the bottom sheet.
* @param showBottomSheet Whether or not to show the bottom sheet, by default this is true assuming
* the showing/hiding will be handled by the caller.
* @param sheetContent Content to display in the bottom sheet. The content is passed the padding
* from the containing [BitwardenScaffold] and a `onDismiss` lambda to be used for manual dismissal
* that will include the dismissal animation.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BitwardenModalBottomSheet(
sheetTitle: String,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
topBarActions: @Composable RowScope.(animatedOnDismiss: () -> Unit) -> Unit = {},
showBottomSheet: Boolean = true,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetContent: @Composable (animatedOnDismiss: () -> Unit) -> Unit,
) {
if (!showBottomSheet) return
ModalBottomSheet(
onDismissRequest = onDismiss,
modifier = modifier.semantics { this.IsBottomSheet = true },
dragHandle = null,
sheetState = sheetState,
contentWindowInsets = {
WindowInsets(left = 0, top = 0, right = 0, bottom = 0)
},
shape = BitwardenTheme.shapes.bottomSheet,
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val animatedOnDismiss = sheetState.createAnimatedDismissAction(onDismiss = onDismiss)
BitwardenScaffold(
topBar = {
BitwardenTopAppBar(
title = sheetTitle,
navigationIcon = NavigationIcon(
navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
onNavigationIconClick = animatedOnDismiss,
navigationIconContentDescription = stringResource(BitwardenString.close),
),
actions = {
topBarActions(animatedOnDismiss)
},
scrollBehavior = scrollBehavior,
minimumHeight = 64.dp,
)
},
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.fillMaxSize(),
) {
sheetContent(animatedOnDismiss)
}
}
}
/**
* SemanticPropertyKey used for Unit tests where checking if the content is part of a bottom sheet.
*/
@VisibleForTesting
val IsBottomSheetKey = SemanticsPropertyKey<Boolean>("IsBottomSheet")
private var SemanticsPropertyReceiver.IsBottomSheet by IsBottomSheetKey
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SheetState.createAnimatedDismissAction(onDismiss: () -> Unit): () -> Unit {
val scope = rememberCoroutineScope()
return {
scope
.launch { this@createAnimatedDismissAction.hide() }
.invokeOnCompletion { onDismiss() }
}
}

View File

@@ -24,10 +24,19 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollToNode
import androidx.compose.ui.test.printToString
import androidx.compose.ui.text.LinkAnnotation
import com.bitwarden.ui.platform.components.bottomsheet.IsBottomSheetKey
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.jupiter.api.assertThrows
/**
* A [SemanticsMatcher] user to find nodes used specifically inside a bottom sheet.
*/
val isBottomSheet: SemanticsMatcher
get() = SemanticsMatcher("Node is used to to indicate it is a bottom sheet.") {
it.config.getOrNull(IsBottomSheetKey) == true
}
/**
* A [SemanticsMatcher] used to find editable text nodes.
*/