PM-17721: Update app dropdown menus (#4646)

This commit is contained in:
David Perez
2025-01-29 14:26:50 -06:00
committed by GitHub
parent 590fc21820
commit 6a1f37b243
9 changed files with 232 additions and 326 deletions

View File

@@ -25,6 +25,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -68,6 +69,7 @@ import kotlinx.collections.immutable.persistentListOf
* @param supportingText A optional supporting text that will appear below the text field.
* @param tooltip A nullable [TooltipData], representing the tooltip icon.
* @param insets Inner padding to be applied withing the card.
* @param textFieldTestTag The optional test tag associated with the inner text field.
* @param cardStyle Indicates the type of card style to be applied.
* @param actionsPadding Padding to be applied to the [actions] block.
* @param actions A lambda containing the set of actions (usually icons or similar) to display
@@ -86,6 +88,7 @@ fun BitwardenMultiSelectButton(
supportingText: String? = null,
tooltip: TooltipData? = null,
insets: PaddingValues = PaddingValues(),
textFieldTestTag: String? = null,
cardStyle: CardStyle? = null,
actionsPadding: PaddingValues = PaddingValues(),
actions: @Composable RowScope.() -> Unit = {},
@@ -161,6 +164,7 @@ fun BitwardenMultiSelectButton(
},
colors = bitwardenTextFieldButtonColors(),
modifier = Modifier
.run { textFieldTestTag?.let { testTag(tag = it) } ?: this }
.weight(weight = 1f)
.fillMaxWidth(),
)

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
import android.content.res.Resources
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
@@ -47,10 +48,9 @@ import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLogoutConfirmationDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTimePickerDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
@@ -68,6 +68,7 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.displayLabel
import com.x8bit.bitwarden.ui.platform.util.minutes
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import kotlinx.collections.immutable.toImmutableList
import java.time.LocalTime
import javax.crypto.Cipher
@@ -497,75 +498,51 @@ private fun SessionTimeoutPolicyRow(
}
}
@Suppress("LongMethod")
@Composable
private fun SessionTimeoutRow(
vaultTimeoutPolicyMinutes: Int?,
selectedVaultTimeoutType: VaultTimeout.Type,
onVaultTimeoutTypeSelect: (VaultTimeout.Type) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalContext.current.resources,
) {
var shouldShowSelectionDialog by remember { mutableStateOf(false) }
var shouldShowNeverTimeoutConfirmationDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = R.string.session_timeout),
onClick = { shouldShowSelectionDialog = true },
val vaultTimeoutOptions = VaultTimeout.Type
.entries
.filter { it.minutes <= (vaultTimeoutPolicyMinutes ?: Int.MAX_VALUE) }
BitwardenMultiSelectButton(
label = stringResource(id = R.string.session_timeout),
options = vaultTimeoutOptions.map { it.displayLabel() }.toImmutableList(),
selectedOption = selectedVaultTimeoutType.displayLabel(),
onOptionSelected = { selectedType ->
val selectedOption = vaultTimeoutOptions.first {
it.displayLabel.toString(resources) == selectedType
}
if (selectedOption == VaultTimeout.Type.NEVER) {
shouldShowNeverTimeoutConfirmationDialog = true
} else {
onVaultTimeoutTypeSelect(selectedOption)
}
},
textFieldTestTag = "SessionTimeoutStatusLabel",
cardStyle = CardStyle.Top(),
modifier = modifier,
) {
Text(
text = selectedVaultTimeoutType.displayLabel(),
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier.testTag("SessionTimeoutStatusLabel"),
)
if (shouldShowNeverTimeoutConfirmationDialog) {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.warning),
message = stringResource(id = R.string.never_lock_warning),
confirmButtonText = stringResource(id = R.string.ok),
dismissButtonText = stringResource(id = R.string.cancel),
onConfirmClick = {
shouldShowNeverTimeoutConfirmationDialog = false
onVaultTimeoutTypeSelect(VaultTimeout.Type.NEVER)
},
onDismissClick = { shouldShowNeverTimeoutConfirmationDialog = false },
onDismissRequest = { shouldShowNeverTimeoutConfirmationDialog = false },
)
}
when {
shouldShowSelectionDialog -> {
val vaultTimeoutOptions = VaultTimeout.Type.entries
.filter {
it.minutes <= (vaultTimeoutPolicyMinutes ?: Int.MAX_VALUE)
}
BitwardenSelectionDialog(
title = stringResource(id = R.string.session_timeout),
onDismissRequest = { shouldShowSelectionDialog = false },
) {
vaultTimeoutOptions.forEach { vaultTimeoutOption ->
BitwardenSelectionRow(
text = vaultTimeoutOption.displayLabel,
onClick = {
shouldShowSelectionDialog = false
val selectedType =
vaultTimeoutOptions.first { it == vaultTimeoutOption }
if (selectedType == VaultTimeout.Type.NEVER) {
shouldShowNeverTimeoutConfirmationDialog = true
} else {
onVaultTimeoutTypeSelect(selectedType)
}
},
isSelected = selectedVaultTimeoutType == vaultTimeoutOption,
)
}
}
}
shouldShowNeverTimeoutConfirmationDialog -> {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.warning),
message = stringResource(id = R.string.never_lock_warning),
confirmButtonText = stringResource(id = R.string.ok),
dismissButtonText = stringResource(id = R.string.cancel),
onConfirmClick = {
shouldShowNeverTimeoutConfirmationDialog = false
onVaultTimeoutTypeSelect(VaultTimeout.Type.NEVER)
},
onDismissClick = { shouldShowNeverTimeoutConfirmationDialog = false },
onDismissRequest = { shouldShowNeverTimeoutConfirmationDialog = false },
)
}
}
}
@Suppress("LongMethod")
@@ -640,7 +617,6 @@ private fun SessionCustomTimeoutRow(
}
}
@Suppress("LongMethod")
@Composable
private fun SessionTimeoutActionRow(
isEnabled: Boolean,
@@ -648,78 +624,52 @@ private fun SessionTimeoutActionRow(
selectedVaultTimeoutAction: VaultTimeoutAction,
onVaultTimeoutActionSelect: (VaultTimeoutAction) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalContext.current.resources,
) {
var shouldShowSelectionDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowLogoutActionConfirmationDialog by rememberSaveable { mutableStateOf(false) }
BitwardenTextRow(
BitwardenMultiSelectButton(
isEnabled = isEnabled,
text = stringResource(id = R.string.session_timeout_action),
description = stringResource(
label = stringResource(id = R.string.session_timeout_action),
options = VaultTimeoutAction.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = selectedVaultTimeoutAction.displayLabel(),
onOptionSelected = { action ->
// The option is not selectable if there's a policy in place.
if (vaultTimeoutPolicyAction != null) return@BitwardenMultiSelectButton
val selectedAction = VaultTimeoutAction.entries.first {
it.displayLabel.toString(resources) == action
}
if (selectedAction == VaultTimeoutAction.LOGOUT) {
shouldShowLogoutActionConfirmationDialog = true
} else {
onVaultTimeoutActionSelect(selectedAction)
}
},
supportingText = stringResource(
id = R.string.set_up_an_unlock_option_to_change_your_vault_timeout_action,
)
.takeUnless { isEnabled },
onClick = {
// The option is not selectable if there's a policy in place.
if (vaultTimeoutPolicyAction != null) return@BitwardenTextRow
shouldShowSelectionDialog = true
},
textFieldTestTag = "SessionTimeoutActionStatusLabel",
cardStyle = CardStyle.Bottom,
modifier = modifier,
) {
Text(
text = selectedVaultTimeoutAction.displayLabel(),
style = BitwardenTheme.typography.labelSmall,
color = if (isEnabled) {
BitwardenTheme.colorScheme.text.primary
} else {
BitwardenTheme.colorScheme.filledButton.foregroundDisabled
},
modifier = Modifier.testTag("SessionTimeoutActionStatusLabel"),
)
}
when {
shouldShowSelectionDialog -> {
BitwardenSelectionDialog(
title = stringResource(id = R.string.vault_timeout_action),
onDismissRequest = { shouldShowSelectionDialog = false },
) {
val vaultTimeoutActionOptions = VaultTimeoutAction.entries
vaultTimeoutActionOptions.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == selectedVaultTimeoutAction,
onClick = {
shouldShowSelectionDialog = false
val selectedAction = vaultTimeoutActionOptions.first { it == option }
if (selectedAction == VaultTimeoutAction.LOGOUT) {
shouldShowLogoutActionConfirmationDialog = true
} else {
onVaultTimeoutActionSelect(selectedAction)
}
},
)
}
}
}
)
shouldShowLogoutActionConfirmationDialog -> {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.warning),
message = stringResource(id = R.string.vault_timeout_log_out_confirmation),
confirmButtonText = stringResource(id = R.string.yes),
dismissButtonText = stringResource(id = R.string.cancel),
onConfirmClick = {
shouldShowLogoutActionConfirmationDialog = false
onVaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT)
},
onDismissClick = {
shouldShowLogoutActionConfirmationDialog = false
},
onDismissRequest = {
shouldShowLogoutActionConfirmationDialog = false
},
)
}
if (shouldShowLogoutActionConfirmationDialog) {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.warning),
message = stringResource(id = R.string.vault_timeout_log_out_confirmation),
confirmButtonText = stringResource(id = R.string.yes),
dismissButtonText = stringResource(id = R.string.cancel),
onConfirmClick = {
shouldShowLogoutActionConfirmationDialog = false
onVaultTimeoutActionSelect(VaultTimeoutAction.LOGOUT)
},
onDismissClick = {
shouldShowLogoutActionConfirmationDialog = false
},
onDismissRequest = {
shouldShowLogoutActionConfirmationDialog = false
},
)
}
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.appearance
import android.content.res.Resources
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -9,7 +10,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
@@ -20,6 +20,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
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.unit.dp
@@ -27,21 +28,18 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.displayLabel
import kotlinx.collections.immutable.toImmutableList
/**
* Displays the appearance screen.
@@ -128,48 +126,30 @@ private fun LanguageSelectionRow(
currentSelection: AppLanguage,
onLanguageSelection: (AppLanguage) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalContext.current.resources,
) {
var languageChangedDialogOption: Text? by rememberSaveable { mutableStateOf(value = null) }
var shouldShowLanguageSelectionDialog by rememberSaveable { mutableStateOf(value = false) }
var languageChangedDialogOption: String? by rememberSaveable { mutableStateOf(value = null) }
BitwardenMultiSelectButton(
label = stringResource(id = R.string.language),
options = AppLanguage.entries.map { it.text() }.toImmutableList(),
selectedOption = currentSelection.text(),
onOptionSelected = { selectedLanguage ->
onLanguageSelection(
AppLanguage.entries.first { selectedLanguage == it.text.toString(resources) },
)
languageChangedDialogOption = selectedLanguage
},
cardStyle = CardStyle.Full,
modifier = modifier,
)
languageChangedDialogOption?.let {
BitwardenBasicDialog(
title = stringResource(id = R.string.language),
message = stringResource(id = R.string.language_change_x_description, it.invoke()),
message = stringResource(id = R.string.language_change_x_description, it),
onDismissRequest = { languageChangedDialogOption = null },
)
}
BitwardenTextRow(
text = stringResource(id = R.string.language),
onClick = { shouldShowLanguageSelectionDialog = true },
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
text = currentSelection.text(),
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
if (shouldShowLanguageSelectionDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.language),
onDismissRequest = { shouldShowLanguageSelectionDialog = false },
) {
AppLanguage.entries.forEach { option ->
BitwardenSelectionRow(
text = option.text,
isSelected = option == currentSelection,
onClick = {
shouldShowLanguageSelectionDialog = false
onLanguageSelection(option)
languageChangedDialogOption = option.text
},
)
}
}
}
}
@Composable
@@ -177,40 +157,19 @@ private fun ThemeSelectionRow(
currentSelection: AppTheme,
onThemeSelection: (AppTheme) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalContext.current.resources,
) {
var shouldShowThemeSelectionDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = R.string.theme),
description = stringResource(id = R.string.theme_description),
onClick = { shouldShowThemeSelectionDialog = true },
BitwardenMultiSelectButton(
label = stringResource(id = R.string.theme),
options = AppTheme.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = currentSelection.displayLabel(),
onOptionSelected = { selectedTheme ->
onThemeSelection(
AppTheme.entries.first { selectedTheme == it.displayLabel.toString(resources) },
)
},
supportingText = stringResource(id = R.string.theme_description),
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
text = currentSelection.displayLabel(),
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
if (shouldShowThemeSelectionDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.theme),
onDismissRequest = { shouldShowThemeSelectionDialog = false },
) {
AppTheme.entries.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == currentSelection,
onClick = {
shouldShowThemeSelectionDialog = false
onThemeSelection(
AppTheme.entries.first { it == option },
)
},
)
}
}
}
)
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.autofill
import android.content.res.Resources
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
@@ -11,7 +12,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
@@ -37,9 +37,8 @@ import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.card.actionCardExitAnimation
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.header.BitwardenListHeaderText
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow
@@ -50,7 +49,7 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.displayLabel
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.toImmutableList
/**
* Displays the auto-fill screen.
@@ -336,41 +335,21 @@ private fun DefaultUriMatchTypeRow(
selectedUriMatchType: UriMatchType,
onUriMatchTypeSelect: (UriMatchType) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalContext.current.resources,
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = R.string.default_uri_match_detection),
description = stringResource(id = R.string.default_uri_match_detection_description),
onClick = { shouldShowDialog = true },
BitwardenMultiSelectButton(
label = stringResource(id = R.string.default_uri_match_detection),
options = UriMatchType.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = selectedUriMatchType.displayLabel(),
onOptionSelected = { selectedOption ->
onUriMatchTypeSelect(
UriMatchType
.entries
.first { it.displayLabel.toString(resources) == selectedOption },
)
},
supportingText = stringResource(id = R.string.default_uri_match_detection_description),
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
text = selectedUriMatchType.displayLabel(),
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary,
)
}
if (shouldShowDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.default_uri_match_detection),
onDismissRequest = { shouldShowDialog = false },
) {
val uriMatchTypes = UriMatchType.entries
uriMatchTypes.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == selectedUriMatchType,
onClick = {
shouldShowDialog = false
onUriMatchTypeSelect(
uriMatchTypes.first { it == option },
)
},
)
}
}
}
)
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.platform.feature.settings.other
import android.content.res.Resources
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -39,15 +40,14 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.collections.immutable.toImmutableList
/**
* Displays the other screen.
@@ -169,7 +169,7 @@ fun OtherScreen(
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.height(height = 8.dp))
ScreenCaptureRow(
currentValue = state.allowScreenCapture,
@@ -182,6 +182,7 @@ fun OtherScreen(
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(height = 16.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
@@ -230,43 +231,24 @@ private fun ClearClipboardFrequencyRow(
currentSelection: ClearClipboardFrequency,
onFrequencySelection: (ClearClipboardFrequency) -> Unit,
modifier: Modifier = Modifier,
resources: Resources = LocalContext.current.resources,
) {
var shouldShowClearClipboardDialog by remember { mutableStateOf(false) }
BitwardenTextRow(
text = stringResource(id = R.string.clear_clipboard),
description = stringResource(id = R.string.clear_clipboard_description),
onClick = { shouldShowClearClipboardDialog = true },
BitwardenMultiSelectButton(
label = stringResource(id = R.string.clear_clipboard),
supportingText = stringResource(id = R.string.clear_clipboard_description),
options = ClearClipboardFrequency.entries.map { it.displayLabel() }.toImmutableList(),
selectedOption = currentSelection.displayLabel(),
onOptionSelected = { selectedFrequency ->
onFrequencySelection(
ClearClipboardFrequency
.entries
.first { it.displayLabel.toString(resources) == selectedFrequency },
)
},
textFieldTestTag = "ClearClipboardAfterLabel",
cardStyle = CardStyle.Full,
modifier = modifier,
) {
Text(
text = currentSelection.displayLabel.invoke(),
style = BitwardenTheme.typography.labelSmall,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier.testTag("ClearClipboardAfterLabel"),
)
}
if (shouldShowClearClipboardDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.clear_clipboard),
onDismissRequest = { shouldShowClearClipboardDialog = false },
) {
ClearClipboardFrequency.entries.forEach { option ->
BitwardenSelectionRow(
text = option.displayLabel,
isSelected = option == currentSelection,
onClick = {
shouldShowClearClipboardDialog = false
onFrequencySelection(
ClearClipboardFrequency.entries.first { it == option },
)
},
)
}
}
}
)
}
@Composable

View File

@@ -5,10 +5,8 @@ import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.hasTextExactly
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.isDisplayed
@@ -552,16 +550,14 @@ class AccountSecurityScreenTest : BaseComposeTest() {
@Test
fun `session timeout should be updated on or off according to state`() {
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.assertTextEquals("Session timeout", "30 minutes")
.assertIsDisplayed()
mutableStateFlow.update { it.copy(vaultTimeout = VaultTimeout.FourHours) }
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "4 hours. Session timeout")
.performScrollTo()
.assertTextEquals("Session timeout", "4 hours")
.assertIsDisplayed()
}
@Test
@@ -569,8 +565,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.performClick()
@@ -634,8 +629,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
}
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.performClick()
@@ -688,8 +682,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.performClick()
@@ -707,8 +700,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.performClick()
@@ -731,8 +723,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.performClick()
@@ -769,8 +760,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.performClick()
@@ -799,8 +789,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onAllNodesWithText("Session timeout")
.filterToOne(hasClickAction())
.onNodeWithContentDescription(label = "30 minutes. Session timeout")
.performScrollTo()
.performClick()
@@ -981,12 +970,12 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Lock. Session timeout action")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Vault timeout action")
.onAllNodesWithText("Session timeout action")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
@@ -1009,12 +998,12 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Lock. Session timeout action")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Vault timeout action")
.onAllNodesWithText("Session timeout action")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
@@ -1033,12 +1022,11 @@ class AccountSecurityScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `on session timeout action dialog Logout click should open a confirmation dialog`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Lock. Session timeout action")
.performScrollTo()
.performClick()
@@ -1077,7 +1065,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
fun `on session timeout action Logout confirmation dialog cancel click should dismiss the dialog`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Lock. Session timeout action")
.performScrollTo()
.performClick()
@@ -1106,7 +1094,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
fun `on session timeout action Logout confirmation dialog Yes click should dismiss the dialog and send VaultTimeoutActionSelect`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Lock. Session timeout action")
.performScrollTo()
.performClick()
@@ -1136,18 +1124,17 @@ class AccountSecurityScreenTest : BaseComposeTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `on session timeout action dialog cancel click should close the dialog`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Lock. Session timeout action")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Vault timeout action")
.onAllNodesWithText("Session timeout action")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
@@ -1163,14 +1150,14 @@ class AccountSecurityScreenTest : BaseComposeTest() {
@Test
fun `session timeout action should be updated according to state`() {
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Lock. Session timeout action")
.performScrollTo()
.assertTextEquals("Session timeout action", "Lock")
.assertIsDisplayed()
mutableStateFlow.update { it.copy(vaultTimeoutAction = VaultTimeoutAction.LOGOUT) }
composeTestRule
.onNodeWithText("Session timeout action")
.onNodeWithContentDescription(label = "Log out. Session timeout action")
.performScrollTo()
.assertTextEquals("Session timeout action", "Log out")
.assertIsDisplayed()
}
@Suppress("MaxLineLength")

View File

@@ -8,7 +8,10 @@ import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.printToLog
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLanguage
@@ -50,7 +53,10 @@ class AppearanceScreenTest : BaseComposeTest() {
@Test
fun `on language row click should display language selection dialog`() {
composeTestRule.onNodeWithText("Language").performClick()
composeTestRule
.onNodeWithContentDescription(label = "Default (System). Language")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Language")
.filterToOne(hasAnyAncestor(isDialog()))
@@ -60,7 +66,10 @@ class AppearanceScreenTest : BaseComposeTest() {
@Test
fun `on language selection dialog item click should send LanguageChange and show dialog`() {
// Clicking the Language row shows the language selection dialog
composeTestRule.onNodeWithText("Language").performClick()
composeTestRule
.onNodeWithContentDescription(label = "Default (System). Language")
.performScrollTo()
.performClick()
// Selecting a language dismisses this dialog and displays the confirmation
composeTestRule
.onAllNodesWithText("Afrikaans")
@@ -93,7 +102,10 @@ class AppearanceScreenTest : BaseComposeTest() {
@Test
fun `on language selection dialog cancel click should dismiss dialog`() {
composeTestRule.onNodeWithText("Language").performClick()
composeTestRule
.onNodeWithContentDescription(label = "Default (System). Language")
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))
@@ -103,7 +115,13 @@ class AppearanceScreenTest : BaseComposeTest() {
@Test
fun `on theme row click should display theme selection dialog`() {
composeTestRule.onNodeWithText("Theme").performClick()
composeTestRule.onRoot().printToLog("Brian")
composeTestRule
.onNodeWithContentDescription(
label = "Default (System). Theme. Change the application's color theme.",
)
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Theme")
.filterToOne(hasAnyAncestor(isDialog()))
@@ -112,7 +130,12 @@ class AppearanceScreenTest : BaseComposeTest() {
@Test
fun `on theme selection dialog item click should send ThemeChange`() {
composeTestRule.onNodeWithText("Theme").performClick()
composeTestRule
.onNodeWithContentDescription(
label = "Default (System). Theme. Change the application's color theme.",
)
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Dark")
.filterToOne(hasAnyAncestor(isDialog()))
@@ -130,7 +153,12 @@ class AppearanceScreenTest : BaseComposeTest() {
@Test
fun `on theme selection dialog cancel click should dismiss dialog`() {
composeTestRule.onNodeWithText("Theme").performClick()
composeTestRule
.onNodeWithContentDescription(
label = "Default (System). Theme. Change the application's color theme.",
)
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Cancel")
.filterToOne(hasAnyAncestor(isDialog()))

View File

@@ -369,7 +369,7 @@ class AutoFillScreenTest : BaseComposeTest() {
fun `on default URI match type click should display dialog`() {
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithText("Default URI match detection")
.onNodeWithContentDescription(label = "Default URI match detection.", substring = true)
.performScrollTo()
.assert(!hasAnyAncestor(isDialog()))
.performClick()
@@ -383,7 +383,7 @@ class AutoFillScreenTest : BaseComposeTest() {
@Test
fun `on default URI match type dialog item click should send DefaultUriMatchTypeSelect and close the dialog`() {
composeTestRule
.onNodeWithText("Default URI match detection")
.onNodeWithContentDescription(label = "Default URI match detection.", substring = true)
.performScrollTo()
.performClick()
@@ -402,11 +402,10 @@ class AutoFillScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `on default URI match type dialog cancel click should close the dialog`() {
composeTestRule
.onNodeWithText("Default URI match detection")
.onNodeWithContentDescription(label = "Default URI match detection.", substring = true)
.performScrollTo()
.performClick()
@@ -422,19 +421,19 @@ class AutoFillScreenTest : BaseComposeTest() {
@Test
fun `default URI match type should update according to state`() {
composeTestRule
.onNodeWithText("Base domain")
.onNodeWithContentDescription(label = "Base domain", substring = true)
.assertExists()
composeTestRule
.onNodeWithText("Starts with")
.onNodeWithContentDescription(label = "Starts with", substring = true)
.assertDoesNotExist()
mutableStateFlow.update {
it.copy(defaultUriMatchType = UriMatchType.STARTS_WITH)
}
composeTestRule
.onNodeWithText("Base domain")
.onNodeWithContentDescription(label = "Base domain", substring = true)
.assertDoesNotExist()
composeTestRule
.onNodeWithText("Starts with")
.onNodeWithContentDescription(label = "Starts with", substring = true)
.assertExists()
}

View File

@@ -88,7 +88,13 @@ class OtherScreenTest : BaseComposeTest() {
@Test
fun `on clear clipboard row click should show show clipboard selection dialog`() {
composeTestRule.onNodeWithText("Clear clipboard").performClick()
composeTestRule
.onNodeWithContentDescription(
label = "Never. Clear clipboard. " +
"Automatically clear copied values from your clipboard.",
)
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("Clear clipboard")
.filterToOne(hasAnyAncestor(isDialog()))
@@ -97,7 +103,13 @@ class OtherScreenTest : BaseComposeTest() {
@Test
fun `on clear clipboard dialog item click should send ClearClipboardFrequencyChange`() {
composeTestRule.onNodeWithText("Clear clipboard").performClick()
composeTestRule
.onNodeWithContentDescription(
label = "Never. Clear clipboard. " +
"Automatically clear copied values from your clipboard.",
)
.performScrollTo()
.performClick()
composeTestRule
.onAllNodesWithText("10 seconds")
.filterToOne(hasAnyAncestor(isDialog()))
@@ -115,7 +127,13 @@ class OtherScreenTest : BaseComposeTest() {
@Test
fun `on clear clipboard dialog cancel should dismiss dialog`() {
composeTestRule.onNodeWithText("Clear clipboard").performClick()
composeTestRule
.onNodeWithContentDescription(
label = "Never. Clear clipboard. " +
"Automatically clear copied values from your clipboard.",
)
.performScrollTo()
.performClick()
composeTestRule.onNodeWithText("Cancel").performClick()
composeTestRule.assertNoDialogExists()
}