From efec5cb4ca492d20a8d127e9f45b6b303e4fce81 Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 31 Mar 2025 10:15:58 -0500 Subject: [PATCH] PM-19653: Add tooltip and subtext tupport for the switch (#4936) --- .../components/toggle/BitwardenSwitch.kt | 153 ++++++++++++++---- .../addedit/VaultAddEditAdditionalOptions.kt | 19 +-- .../addedit/VaultAddEditCustomField.kt | 53 ++---- 3 files changed, 146 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt index 4d63a3b435..06c3e567ba 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/toggle/BitwardenSwitch.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -23,7 +24,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.cardStyle @@ -31,6 +34,8 @@ import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.x8bit.bitwarden.ui.platform.components.model.CardStyle +import com.x8bit.bitwarden.ui.platform.components.model.TooltipData +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -42,8 +47,10 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * @param onCheckedChange A lambda that is invoked when the switch's state changes. * @param cardStyle Indicates the type of card style to be applied. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param subtext The text to be displayed under the [label]. * @param supportingText An optional supporting text to be displayed below the [label]. * @param contentDescription A description of the switch's UI for accessibility purposes. + * @param tooltip The data required to display a tooltip. * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but * comes with some additional visual changes. @@ -58,8 +65,10 @@ fun BitwardenSwitch( onCheckedChange: ((Boolean) -> Unit)?, cardStyle: CardStyle?, modifier: Modifier = Modifier, + subtext: String? = null, supportingText: String? = null, contentDescription: String? = null, + tooltip: TooltipData? = null, readOnly: Boolean = false, enabled: Boolean = true, actions: (@Composable RowScope.() -> Unit)? = null, @@ -67,9 +76,11 @@ fun BitwardenSwitch( BitwardenSwitch( modifier = modifier, label = label.toAnnotatedString(), + subtext = subtext, isChecked = isChecked, onCheckedChange = onCheckedChange, contentDescription = contentDescription, + tooltip = tooltip, readOnly = readOnly, enabled = enabled, cardStyle = cardStyle, @@ -98,8 +109,10 @@ fun BitwardenSwitch( * @param onCheckedChange A lambda that is invoked when the switch's state changes. * @param cardStyle Indicates the type of card style to be applied. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param subtext The text to be displayed under the [label]. * @param supportingText An optional supporting text to be displayed below the [label]. * @param contentDescription A description of the switch's UI for accessibility purposes. + * @param tooltip The data required to display a tooltip. * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but * comes with some additional visual changes. @@ -114,8 +127,10 @@ fun BitwardenSwitch( onCheckedChange: ((Boolean) -> Unit)?, cardStyle: CardStyle?, modifier: Modifier = Modifier, + subtext: String? = null, supportingText: String? = null, contentDescription: String? = null, + tooltip: TooltipData? = null, readOnly: Boolean = false, enabled: Boolean = true, actions: (@Composable RowScope.() -> Unit)? = null, @@ -123,9 +138,11 @@ fun BitwardenSwitch( BitwardenSwitch( modifier = modifier, label = label, + subtext = subtext, isChecked = isChecked, onCheckedChange = onCheckedChange, contentDescription = contentDescription, + tooltip = tooltip, readOnly = readOnly, enabled = enabled, cardStyle = cardStyle, @@ -154,6 +171,7 @@ fun BitwardenSwitch( * @param onCheckedChange A lambda that is invoked when the switch's state changes. * @param cardStyle Indicates the type of card style to be applied. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param subtext The text to be displayed under the [label]. * @param contentDescription A description of the switch's UI for accessibility purposes. * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but @@ -170,6 +188,7 @@ fun BitwardenSwitch( onCheckedChange: ((Boolean) -> Unit)?, cardStyle: CardStyle?, modifier: Modifier = Modifier, + subtext: String? = null, contentDescription: String? = null, readOnly: Boolean = false, enabled: Boolean = true, @@ -179,6 +198,7 @@ fun BitwardenSwitch( BitwardenSwitch( modifier = modifier, label = label.toAnnotatedString(), + subtext = subtext, isChecked = isChecked, onCheckedChange = onCheckedChange, contentDescription = contentDescription, @@ -198,7 +218,9 @@ fun BitwardenSwitch( * @param onCheckedChange A lambda that is invoked when the switch's state changes. * @param cardStyle Indicates the type of card style to be applied. * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param subtext The text to be displayed under the [label]. * @param contentDescription A description of the switch's UI for accessibility purposes. + * @param tooltip The data required to display a tooltip. * @param readOnly Disables the click functionality without modifying the other UI characteristics. * @param enabled Whether or not this switch is enabled. This is similar to setting [readOnly] but * comes with some additional visual changes. @@ -207,7 +229,7 @@ fun BitwardenSwitch( * defining the layout of the actions. * @param supportingContent A lambda containing content directly below the label. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun BitwardenSwitch( label: AnnotatedString, @@ -215,7 +237,9 @@ fun BitwardenSwitch( onCheckedChange: ((Boolean) -> Unit)?, cardStyle: CardStyle?, modifier: Modifier = Modifier, + subtext: String? = null, contentDescription: String? = null, + tooltip: TooltipData? = null, readOnly: Boolean = false, enabled: Boolean = true, supportingContentPadding: PaddingValues = PaddingValues(vertical = 12.dp, horizontal = 16.dp), @@ -240,26 +264,50 @@ fun BitwardenSwitch( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .defaultMinSize(minHeight = 36.dp) - .padding(horizontal = 16.dp), + modifier = Modifier.defaultMinSize(minHeight = 36.dp), ) { + Spacer(modifier = Modifier.width(width = 16.dp)) Row( modifier = Modifier.weight(weight = 1f), verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = label, - style = BitwardenTheme.typography.bodyLarge, - color = if (enabled) { - BitwardenTheme.colorScheme.text.primary - } else { - BitwardenTheme.colorScheme.filledButton.foregroundDisabled - }, - modifier = Modifier.testTag(tag = "SwitchText"), - ) - - actions?.invoke(this) + Column(modifier = Modifier.weight(weight = 1f, fill = false)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = label, + style = BitwardenTheme.typography.bodyLarge, + color = if (enabled) { + BitwardenTheme.colorScheme.text.primary + } else { + BitwardenTheme.colorScheme.filledButton.foregroundDisabled + }, + modifier = Modifier.testTag(tag = "SwitchText"), + ) + tooltip?.let { + ToolTip( + tooltip = it, + isVisible = subtext != null, + size = 16.dp, + ) + } + } + subtext?.let { + Spacer(modifier = Modifier.height(height = 2.dp)) + Text( + text = it, + style = BitwardenTheme.typography.bodyMedium, + color = if (enabled) { + BitwardenTheme.colorScheme.text.secondary + } else { + BitwardenTheme.colorScheme.filledButton.foregroundDisabled + }, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(tag = "SwitchSubtext"), + ) + } + } + tooltip?.let { ToolTip(tooltip = it, isVisible = subtext == null) } } Spacer(modifier = Modifier.width(width = 16.dp)) Switch( @@ -271,20 +319,13 @@ fun BitwardenSwitch( onCheckedChange = null, colors = bitwardenSwitchColors(), ) + actions?.let { BitwardenRowOfActions(actions = it) } + Spacer(modifier = Modifier.width(width = if (actions == null) 16.dp else 4.dp)) } supportingContent ?.let { content -> - Spacer(modifier = Modifier.height(height = 12.dp)) - BitwardenHorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp), - ) - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .defaultMinSize(minHeight = 48.dp) - .padding(paddingValues = supportingContentPadding), + SupportingContent( + paddingValues = supportingContentPadding, content = content, ) } @@ -292,6 +333,45 @@ fun BitwardenSwitch( } } +@Composable +private fun ColumnScope.SupportingContent( + paddingValues: PaddingValues, + content: @Composable ColumnScope.() -> Unit, +) { + Spacer(modifier = Modifier.height(height = 12.dp)) + BitwardenHorizontalDivider( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp), + ) + Column( + modifier = Modifier + .defaultMinSize(minHeight = 48.dp) + .padding(paddingValues = paddingValues), + verticalArrangement = Arrangement.Center, + content = content, + ) +} + +@Composable +private fun RowScope.ToolTip( + tooltip: TooltipData, + isVisible: Boolean, + size: Dp = 48.dp, +) { + if (!isVisible) return + Spacer(modifier = Modifier.width(width = 8.dp)) + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_question_circle_small, + contentDescription = tooltip.contentDescription, + onClick = tooltip.onClick, + contentColor = BitwardenTheme.colorScheme.icon.secondary, + modifier = Modifier + .size(size = size) + .testTag(tag = "SwitchTooltip"), + ) +} + @Preview @Composable private fun BitwardenSwitch_preview() { @@ -314,22 +394,37 @@ private fun BitwardenSwitch_preview() { supportingText = "description", isChecked = true, onCheckedChange = {}, + tooltip = TooltipData( + onClick = { }, + contentDescription = "content description", + ), actions = { BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_question_circle, + vectorIconRes = R.drawable.ic_generate, contentDescription = "content description", onClick = {}, ) }, cardStyle = CardStyle.Middle(), ) + BitwardenSwitch( + label = "Label", + supportingText = "description", + isChecked = true, + onCheckedChange = {}, + tooltip = TooltipData( + onClick = { }, + contentDescription = "content description", + ), + cardStyle = CardStyle.Middle(), + ) BitwardenSwitch( label = "Label", isChecked = false, onCheckedChange = {}, actions = { BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_question_circle, + vectorIconRes = R.drawable.ic_generate, contentDescription = "content description", onClick = {}, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditAdditionalOptions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditAdditionalOptions.kt index 72fe83c316..b60e9bda50 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditAdditionalOptions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditAdditionalOptions.kt @@ -13,13 +13,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin -import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.header.BitwardenExpandingHeader 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.model.TooltipData import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch -import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType @@ -79,16 +78,12 @@ fun LazyListScope.vaultAddEditAdditionalOptions( label = stringResource(id = R.string.password_prompt), isChecked = commonState.masterPasswordReprompt, onCheckedChange = commonTypeHandlers.onToggleMasterPasswordReprompt, - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_question_circle_small, - contentDescription = stringResource( - id = R.string.master_password_re_prompt_help, - ), - onClick = commonTypeHandlers.onTooltipClick, - contentColor = BitwardenTheme.colorScheme.icon.secondary, - ) - }, + tooltip = TooltipData( + onClick = commonTypeHandlers.onTooltipClick, + contentDescription = stringResource( + id = R.string.master_password_re_prompt_help, + ), + ), cardStyle = CardStyle.Full, modifier = Modifier .testTag(tag = "MasterPasswordRepromptToggle") diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt index b640cad1a5..56615713e2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditCustomField.kt @@ -1,22 +1,17 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.cardStyle import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTextEntryDialog @@ -25,7 +20,6 @@ import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectB import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField import com.x8bit.bitwarden.ui.platform.components.model.CardStyle -import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType @@ -155,38 +149,21 @@ private fun CustomFieldBoolean( cardStyle: CardStyle, modifier: Modifier = Modifier, ) { - Row( - modifier = modifier - .semantics(mergeDescendants = true) {} - .defaultMinSize(minHeight = 60.dp) - .cardStyle( - cardStyle = cardStyle, - onClick = { onValueChanged(!value) }, - paddingEnd = 4.dp, - paddingTop = 6.dp, - paddingBottom = 6.dp, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - BitwardenSwitch( - label = label, - isChecked = value, - onCheckedChange = null, - cardStyle = null, - modifier = Modifier.weight(1f), - ) - - BitwardenRowOfActions( - actions = { - BitwardenStandardIconButton( - vectorIconRes = R.drawable.ic_cog, - contentDescription = stringResource(id = R.string.edit), - onClick = onEditValue, - modifier = Modifier.testTag("CustomFieldSettingsButton"), - ) - }, - ) - } + BitwardenSwitch( + modifier = modifier, + label = label, + isChecked = value, + onCheckedChange = onValueChanged, + cardStyle = cardStyle, + actions = { + BitwardenStandardIconButton( + vectorIconRes = R.drawable.ic_cog, + contentDescription = stringResource(id = R.string.edit), + onClick = onEditValue, + modifier = Modifier.testTag(tag = "CustomFieldSettingsButton"), + ) + }, + ) } /**