From ccd4fd9aba946333568ea61864395e4ababa8571 Mon Sep 17 00:00:00 2001 From: David Perez Date: Tue, 19 Nov 2024 09:03:23 -0600 Subject: [PATCH] PM-15038: Update custom switches to use standard component (#4327) --- .../createaccount/CreateAccountScreen.kt | 65 ++----- .../StartRegistrationScreen.kt | 54 ++---- .../components/toggle/BitwardenSwitch.kt | 167 ++++++++++++++++-- 3 files changed, 182 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt index 1b61b6b42..a3b47439c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt @@ -1,14 +1,11 @@ package com.x8bit.bitwarden.ui.auth.feature.createaccount import android.widget.Toast -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -17,31 +14,23 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.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.semantics.semantics -import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.net.toUri @@ -76,7 +65,6 @@ 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.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch -import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager @@ -283,6 +271,9 @@ fun CreateAccountScreen( onPrivacyPolicyClick = remember(viewModel) { { viewModel.trySendAction(PrivacyPolicyClick) } }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.navigationBarsPadding()) } @@ -290,52 +281,24 @@ fun CreateAccountScreen( } @OptIn(ExperimentalLayoutApi::class) -@Suppress("LongMethod") @Composable private fun TermsAndPrivacySwitch( isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onTermsClick: () -> Unit, onPrivacyPolicyClick: () -> Unit, + modifier: Modifier = Modifier, ) { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .semantics(mergeDescendants = true) { - testTag = "AcceptPoliciesToggle" - toggleableState = ToggleableState(isChecked) - } - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - color = BitwardenTheme.colorScheme.background.pressed, - ), - onClick = { onCheckedChange.invoke(!isChecked) }, - ) - .padding(start = 16.dp) - .fillMaxWidth(), - ) { - Switch( - modifier = Modifier - .height(32.dp) - .width(52.dp), - checked = isChecked, - onCheckedChange = null, - colors = bitwardenSwitchColors(), - ) - Column(Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp)) { - Text( - text = stringResource(id = R.string.accept_policies), - style = BitwardenTheme.typography.bodyLarge, - color = BitwardenTheme.colorScheme.text.primary, - ) + BitwardenSwitch( + modifier = modifier, + label = stringResource(id = R.string.accept_policies), + isChecked = isChecked, + contentDescription = "AcceptPoliciesToggle", + onCheckedChange = onCheckedChange, + subContent = { FlowRow( horizontalArrangement = Arrangement.Start, - modifier = Modifier - .padding(end = 16.dp) - .fillMaxWidth() - .wrapContentHeight(), + modifier = Modifier.fillMaxWidth(), ) { BitwardenClickableText( label = stringResource(id = R.string.terms_of_service), @@ -357,6 +320,6 @@ private fun TermsAndPrivacySwitch( innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp), ) } - } - } + }, + ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt index 4d1a1f45f..57c3d271b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt @@ -2,8 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.startregistration import android.widget.Toast import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -16,15 +14,12 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -38,8 +33,6 @@ import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag -import androidx.compose.ui.semantics.toggleableState -import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign @@ -70,7 +63,7 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.dropdown.EnvironmentSelector 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.toggle.color.bitwardenSwitchColors +import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager @@ -277,7 +270,9 @@ private fun StartRegistrationContent( isChecked = isReceiveMarketingEmailsToggled, onCheckedChange = handler.onReceiveMarketingEmailsToggle, onUnsubscribeClick = handler.onUnsubscribeMarketingEmailsClick, - modifier = Modifier.standardHorizontalMargin(), + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), ) Spacer(modifier = Modifier.height(24.dp)) } @@ -361,7 +356,6 @@ private fun TermsAndPrivacyText( } } -@Suppress("LongMethod") @Composable private fun ReceiveMarketingEmailsSwitch( isChecked: Boolean, @@ -373,7 +367,9 @@ private fun ReceiveMarketingEmailsSwitch( @Suppress("MaxLineLength") val annotatedLinkString = createClickableAnnotatedString( - mainString = stringResource(id = R.string.get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time), + mainString = stringResource( + id = R.string.get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time, + ), highlights = listOf( ClickableTextHighlight( textToHighlight = unsubscribeString, @@ -381,13 +377,9 @@ private fun ReceiveMarketingEmailsSwitch( ), ), ) - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, + BitwardenSwitch( modifier = modifier .semantics(mergeDescendants = true) { - testTag = "ReceiveMarketingEmailsToggle" - toggleableState = ToggleableState(isChecked) customActions = listOf( CustomAccessibilityAction( label = unsubscribeString, @@ -397,30 +389,12 @@ private fun ReceiveMarketingEmailsSwitch( }, ), ) - } - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple( - color = BitwardenTheme.colorScheme.background.pressed, - ), - onClick = { onCheckedChange.invoke(!isChecked) }, - ) - .fillMaxWidth(), - ) { - Switch( - modifier = Modifier - .height(32.dp) - .width(52.dp), - checked = isChecked, - onCheckedChange = null, - colors = bitwardenSwitchColors(), - ) - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = annotatedLinkString, - style = BitwardenTheme.typography.bodyMedium, - ) - } + }, + label = annotatedLinkString, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = "ReceiveMarketingEmailsToggle", + ) } @PreviewScreenSizes 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 a00cdbe6f..f82ee2c32 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 @@ -22,15 +22,17 @@ import androidx.compose.ui.semantics.contentDescription 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +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.toggle.color.bitwardenSwitchColors import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme /** - * A wide custom switch composable + * A custom switch composable * * @param label The descriptive text label to be displayed adjacent to the switch. * @param isChecked The current state of the switch (either checked or unchecked). @@ -45,7 +47,6 @@ import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in * defining the layout of the actions. */ -@Suppress("LongMethod") @Composable fun BitwardenSwitch( label: String, @@ -57,6 +58,154 @@ fun BitwardenSwitch( readOnly: Boolean = false, enabled: Boolean = true, actions: (@Composable RowScope.() -> Unit)? = null, +) { + BitwardenSwitch( + modifier = modifier, + label = label.toAnnotatedString(), + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = contentDescription, + readOnly = readOnly, + enabled = enabled, + actions = actions, + subContent = { + description?.let { + Text( + text = it, + style = BitwardenTheme.typography.bodyMedium, + color = if (enabled) { + BitwardenTheme.colorScheme.text.secondary + } else { + BitwardenTheme.colorScheme.filledButton.foregroundDisabled + }, + ) + } + }, + ) +} + +/** + * A custom switch composable + * + * @param label The descriptive text label to be displayed adjacent to the switch. + * @param isChecked The current state of the switch (either checked or unchecked). + * @param onCheckedChange A lambda that is invoked when the switch's state changes. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @param description An optional description label to be displayed below 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 + * comes with some additional visual changes. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + */ +@Composable +fun BitwardenSwitch( + label: AnnotatedString, + isChecked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + description: String? = null, + contentDescription: String? = null, + readOnly: Boolean = false, + enabled: Boolean = true, + actions: (@Composable RowScope.() -> Unit)? = null, +) { + BitwardenSwitch( + modifier = modifier, + label = label, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = contentDescription, + readOnly = readOnly, + enabled = enabled, + actions = actions, + subContent = { + description?.let { + Text( + text = it, + style = BitwardenTheme.typography.bodyMedium, + color = if (enabled) { + BitwardenTheme.colorScheme.text.secondary + } else { + BitwardenTheme.colorScheme.filledButton.foregroundDisabled + }, + ) + } + }, + ) +} + +/** + * A custom switch composable + * + * @param label The descriptive text label to be displayed adjacent to the switch. + * @param isChecked The current state of the switch (either checked or unchecked). + * @param onCheckedChange A lambda that is invoked when the switch's state changes. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @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 + * comes with some additional visual changes. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + * @param subContent A lambda containing content directly below the label. + */ +@Composable +fun BitwardenSwitch( + label: String, + isChecked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + contentDescription: String? = null, + readOnly: Boolean = false, + enabled: Boolean = true, + actions: (@Composable RowScope.() -> Unit)? = null, + subContent: @Composable () -> Unit, +) { + BitwardenSwitch( + modifier = modifier, + label = label.toAnnotatedString(), + isChecked = isChecked, + onCheckedChange = onCheckedChange, + contentDescription = contentDescription, + readOnly = readOnly, + enabled = enabled, + actions = actions, + subContent = subContent, + ) +} + +/** + * A custom switch composable + * + * @param label The descriptive text label to be displayed adjacent to the switch. + * @param isChecked The current state of the switch (either checked or unchecked). + * @param onCheckedChange A lambda that is invoked when the switch's state changes. + * @param modifier A [Modifier] that you can use to apply custom modifications to the composable. + * @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 + * comes with some additional visual changes. + * @param actions A lambda containing the set of actions (usually icons or similar) to display + * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in + * defining the layout of the actions. + * @param subContent A lambda containing content directly below the label. + */ +@Suppress("LongMethod") +@Composable +fun BitwardenSwitch( + label: AnnotatedString, + isChecked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + contentDescription: String? = null, + readOnly: Boolean = false, + enabled: Boolean = true, + actions: (@Composable RowScope.() -> Unit)? = null, + subContent: @Composable () -> Unit, ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -95,17 +244,7 @@ fun BitwardenSwitch( }, modifier = Modifier.testTag(tag = "SwitchText"), ) - description?.let { - Text( - text = it, - style = BitwardenTheme.typography.bodyMedium, - color = if (enabled) { - BitwardenTheme.colorScheme.text.secondary - } else { - BitwardenTheme.colorScheme.filledButton.foregroundDisabled - }, - ) - } + subContent() } actions @@ -131,6 +270,7 @@ private fun BitwardenSwitch_preview() { Column { BitwardenSwitch( label = "Label", + description = "description", isChecked = true, onCheckedChange = {}, ) @@ -141,6 +281,7 @@ private fun BitwardenSwitch_preview() { ) BitwardenSwitch( label = "Label", + description = "description", isChecked = true, onCheckedChange = {}, actions = {