mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-13943 : PT1 Custom snackbar UI (#4135)
This commit is contained in:
parent
c5a266dfc0
commit
a23fc319de
8 changed files with 243 additions and 45 deletions
|
@ -12,7 +12,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
|
@ -22,7 +21,6 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -36,7 +34,9 @@ import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButtonWi
|
|||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
|
@ -55,18 +55,14 @@ fun MasterPasswordGeneratorScreen(
|
|||
viewModel: MasterPasswordGeneratorViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val snackbarHostState = remember {
|
||||
SnackbarHostState()
|
||||
}
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
MasterPasswordGeneratorEvent.NavigateBack -> onNavigateBack()
|
||||
MasterPasswordGeneratorEvent.NavigateToPreventLockout -> onNavigateToPreventLockout()
|
||||
is MasterPasswordGeneratorEvent.ShowSnackbar -> {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = event.text.toString(resources),
|
||||
snackbarData = BitwardenSnackbarData(message = event.text),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
}
|
||||
|
@ -100,7 +96,7 @@ fun MasterPasswordGeneratorScreen(
|
|||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
BitwardenSnackbarHost(hostState = snackbarHostState)
|
||||
BitwardenSnackbarHost(bitwardenHostState = snackbarHostState)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
|
|
|
@ -2,10 +2,13 @@ package com.x8bit.bitwarden.ui.platform.components.button
|
|||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -26,6 +29,7 @@ fun BitwardenOutlinedButton(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
colors: BitwardenOutlinedButtonColors = bitwardenOutlinedButtonColors(),
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
|
@ -36,13 +40,13 @@ fun BitwardenOutlinedButton(
|
|||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
colors = bitwardenOutlinedButtonColors(),
|
||||
colors = colors.materialButtonColors,
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = if (isEnabled) {
|
||||
BitwardenTheme.colorScheme.outlineButton.border
|
||||
colors.outlineBorderColor
|
||||
} else {
|
||||
BitwardenTheme.colorScheme.outlineButton.borderDisabled
|
||||
colors.outlinedDisabledBorderColor
|
||||
},
|
||||
),
|
||||
) {
|
||||
|
@ -53,6 +57,16 @@ fun BitwardenOutlinedButton(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Colors for a [BitwardenOutlinedButton].
|
||||
*/
|
||||
@Immutable
|
||||
data class BitwardenOutlinedButtonColors(
|
||||
val materialButtonColors: ButtonColors,
|
||||
val outlineBorderColor: Color,
|
||||
val outlinedDisabledBorderColor: Color,
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenOutlinedButton_preview_isEnabled() {
|
||||
|
|
|
@ -33,6 +33,7 @@ fun BitwardenOutlinedButtonWithIcon(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isEnabled: Boolean = true,
|
||||
colors: BitwardenOutlinedButtonColors = bitwardenOutlinedButtonColors(),
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
|
@ -43,13 +44,13 @@ fun BitwardenOutlinedButtonWithIcon(
|
|||
vertical = 10.dp,
|
||||
horizontal = 24.dp,
|
||||
),
|
||||
colors = bitwardenOutlinedButtonColors(),
|
||||
colors = colors.materialButtonColors,
|
||||
border = BorderStroke(
|
||||
width = 1.dp,
|
||||
color = if (isEnabled) {
|
||||
BitwardenTheme.colorScheme.outlineButton.border
|
||||
colors.outlineBorderColor
|
||||
} else {
|
||||
BitwardenTheme.colorScheme.outlineButton.borderDisabled
|
||||
colors.outlinedDisabledBorderColor
|
||||
},
|
||||
),
|
||||
) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.platform.components.button.color
|
|||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButtonColors
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
|
@ -42,12 +43,21 @@ fun bitwardenFilledTonalButtonColors(): ButtonColors = ButtonColors(
|
|||
* Provides a default set of Bitwarden-styled colors for an outlined button.
|
||||
*/
|
||||
@Composable
|
||||
fun bitwardenOutlinedButtonColors(): ButtonColors = ButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = BitwardenTheme.colorScheme.outlineButton.foreground,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled,
|
||||
)
|
||||
fun bitwardenOutlinedButtonColors(
|
||||
contentColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground,
|
||||
outlineColor: Color = BitwardenTheme.colorScheme.outlineButton.border,
|
||||
outlineColorDisabled: Color = BitwardenTheme.colorScheme.outlineButton.borderDisabled,
|
||||
): BitwardenOutlinedButtonColors =
|
||||
BitwardenOutlinedButtonColors(
|
||||
materialButtonColors = ButtonColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = contentColor,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
disabledContentColor = BitwardenTheme.colorScheme.outlineButton.foregroundDisabled,
|
||||
),
|
||||
outlineBorderColor = outlineColor,
|
||||
outlinedDisabledBorderColor = outlineColorDisabled,
|
||||
)
|
||||
|
||||
/**
|
||||
* Provides a default set of Bitwarden-styled colors for an outlined error button.
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components.snackbar
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.color.bitwardenOutlinedButtonColors
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Custom snackbar for Bitwarden.
|
||||
* Shows a message with an optional actions and title.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenSnackbar(
|
||||
bitwardenSnackbarData: BitwardenSnackbarData,
|
||||
modifier: Modifier = Modifier,
|
||||
onDismiss: () -> Unit = {},
|
||||
onActionClick: () -> Unit = {},
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.padding(12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = BitwardenTheme.colorScheme.background.alert,
|
||||
shape = BitwardenTheme.shapes.snackbar,
|
||||
)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Column {
|
||||
bitwardenSnackbarData.messageHeader?.let {
|
||||
Text(
|
||||
text = it(),
|
||||
color = BitwardenTheme.colorScheme.text.reversed,
|
||||
style = BitwardenTheme.typography.titleSmall,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
Text(
|
||||
text = bitwardenSnackbarData.message(),
|
||||
color = BitwardenTheme.colorScheme.text.reversed,
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
)
|
||||
bitwardenSnackbarData.actionLabel?.let {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = it(),
|
||||
onClick = onActionClick,
|
||||
colors = bitwardenOutlinedButtonColors(
|
||||
contentColor = BitwardenTheme.colorScheme.text.reversed,
|
||||
outlineColor = BitwardenTheme
|
||||
.colorScheme
|
||||
.outlineButton
|
||||
.borderReversed,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (bitwardenSnackbarData.withDismissAction) {
|
||||
Spacer(Modifier.weight(1f))
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
content = {
|
||||
Icon(
|
||||
rememberVectorPainter(R.drawable.ic_close),
|
||||
contentDescription = stringResource(R.string.close),
|
||||
tint = BitwardenTheme.colorScheme.icon.reversed,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenCustomSnackbar_preview() {
|
||||
BitwardenTheme {
|
||||
Surface {
|
||||
BitwardenSnackbar(
|
||||
BitwardenSnackbarData(
|
||||
messageHeader = "Header".asText(),
|
||||
message = "Message".asText(),
|
||||
actionLabel = "Action".asText(),
|
||||
withDismissAction = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +1,32 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components.snackbar
|
||||
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* A custom Bitwarden-themed snackbar.
|
||||
*
|
||||
* @param hostState The state of this snackbar.
|
||||
* @param modifier The [Modifier] to be applied to this radio button.
|
||||
* @param bitwardenHostState The state of this snackbar.
|
||||
* @param modifier The [Modifier] to be applied to the [SnackbarHost].
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenSnackbarHost(
|
||||
hostState: SnackbarHostState,
|
||||
bitwardenHostState: BitwardenSnackbarHostState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SnackbarHost(
|
||||
hostState = hostState,
|
||||
hostState = bitwardenHostState.snackbarHostState,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
shape = BitwardenTheme.shapes.snackbar,
|
||||
containerColor = BitwardenTheme.colorScheme.background.alert,
|
||||
contentColor = BitwardenTheme.colorScheme.text.reversed,
|
||||
actionColor = BitwardenTheme.colorScheme.background.alert,
|
||||
actionContentColor = BitwardenTheme.colorScheme.icon.reversed,
|
||||
dismissActionContentColor = BitwardenTheme.colorScheme.icon.reversed,
|
||||
)
|
||||
) { snackbarData ->
|
||||
val message = snackbarData.visuals.message
|
||||
val currentCustomSnackbarData = bitwardenHostState.currentSnackbarData
|
||||
if (currentCustomSnackbarData?.key == message) {
|
||||
BitwardenSnackbar(
|
||||
bitwardenSnackbarData = currentCustomSnackbarData,
|
||||
onDismiss = snackbarData::dismiss,
|
||||
onActionClick = snackbarData::performAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components.snackbar
|
||||
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
|
||||
/**
|
||||
* A custom state holder for [BitwardenSnackbarData] and manging a snackbar host with the
|
||||
* passed in [SnackbarHostState].
|
||||
*/
|
||||
@Stable
|
||||
class BitwardenSnackbarHostState(
|
||||
val snackbarHostState: SnackbarHostState,
|
||||
) {
|
||||
/**
|
||||
* The current snackbar data to be displayed.
|
||||
*/
|
||||
var currentSnackbarData: BitwardenSnackbarData? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
/**
|
||||
* Shows a snackbar with the given [snackbarData]. Passes the [BitwardenSnackbarData.key]
|
||||
* through the message parameter of the [SnackbarHostState.showSnackbar] method. This key
|
||||
* can be used to identify the correct snackbar data to show in the host.
|
||||
*/
|
||||
suspend fun showSnackbar(
|
||||
snackbarData: BitwardenSnackbarData,
|
||||
duration: SnackbarDuration = SnackbarDuration.Short,
|
||||
): SnackbarResult {
|
||||
currentSnackbarData = snackbarData
|
||||
return snackbarHostState
|
||||
.showSnackbar(message = snackbarData.key, duration = duration)
|
||||
.also { currentSnackbarData = null }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models possible data to show in a custom bitwarden snackbar.
|
||||
* @property message The text to show in the snackbar.
|
||||
* @property messageHeader The optional title text to show.
|
||||
* @property actionLabel The optional text to show in the action button.
|
||||
* @property withDismissAction Whether to show the dismiss action.
|
||||
* @property key The unique key for the [BitwardenSnackbarData].
|
||||
*/
|
||||
@Immutable
|
||||
data class BitwardenSnackbarData(
|
||||
val message: Text,
|
||||
val messageHeader: Text? = null,
|
||||
val actionLabel: Text? = null,
|
||||
val withDismissAction: Boolean = false,
|
||||
) {
|
||||
val key: String = this.hashCode().toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [BitwardenSnackbarHostState] that is remembered across compositions.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberBitwardenSnackbarHostState(
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) = remember {
|
||||
BitwardenSnackbarHostState(snackbarHostState)
|
||||
}
|
|
@ -13,7 +13,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
|
@ -24,7 +23,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
@ -55,7 +53,9 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
|||
import com.x8bit.bitwarden.ui.platform.components.segment.BitwardenSegmentedButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.segment.SegmentedButtonState
|
||||
import com.x8bit.bitwarden.ui.platform.components.slider.BitwardenSlider
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.rememberBitwardenSnackbarHostState
|
||||
import com.x8bit.bitwarden.ui.platform.components.stepper.BitwardenStepper
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation
|
||||
|
@ -86,9 +86,7 @@ fun GeneratorScreen(
|
|||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
|
||||
LivecycleEventEffect { _, event ->
|
||||
when (event) {
|
||||
|
@ -106,7 +104,7 @@ fun GeneratorScreen(
|
|||
|
||||
is GeneratorEvent.ShowSnackbar -> {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = event.message(resources).toString(),
|
||||
snackbarData = BitwardenSnackbarData(message = event.message),
|
||||
duration = SnackbarDuration.Short,
|
||||
)
|
||||
}
|
||||
|
@ -199,7 +197,7 @@ fun GeneratorScreen(
|
|||
}
|
||||
},
|
||||
snackbarHost = {
|
||||
BitwardenSnackbarHost(hostState = snackbarHostState)
|
||||
BitwardenSnackbarHost(bitwardenHostState = snackbarHostState)
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { innerPadding ->
|
||||
|
|
Loading…
Reference in a new issue