PM-15038: Update custom switches to use standard component (#4327)

This commit is contained in:
David Perez 2024-11-19 09:03:23 -06:00 committed by GitHub
parent 2d15c4864f
commit ccd4fd9aba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 182 additions and 104 deletions

View file

@ -1,14 +1,11 @@
package com.x8bit.bitwarden.ui.auth.feature.createaccount package com.x8bit.bitwarden.ui.auth.feature.createaccount
import android.widget.Toast 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth 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.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource 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.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri 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.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText 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.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.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@ -283,6 +271,9 @@ fun CreateAccountScreen(
onPrivacyPolicyClick = remember(viewModel) { onPrivacyPolicyClick = remember(viewModel) {
{ viewModel.trySendAction(PrivacyPolicyClick) } { viewModel.trySendAction(PrivacyPolicyClick) }
}, },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) )
Spacer(modifier = Modifier.navigationBarsPadding()) Spacer(modifier = Modifier.navigationBarsPadding())
} }
@ -290,52 +281,24 @@ fun CreateAccountScreen(
} }
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Suppress("LongMethod")
@Composable @Composable
private fun TermsAndPrivacySwitch( private fun TermsAndPrivacySwitch(
isChecked: Boolean, isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit,
onTermsClick: () -> Unit, onTermsClick: () -> Unit,
onPrivacyPolicyClick: () -> Unit, onPrivacyPolicyClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Row( BitwardenSwitch(
horizontalArrangement = Arrangement.Start, modifier = modifier,
verticalAlignment = Alignment.CenterVertically, label = stringResource(id = R.string.accept_policies),
modifier = Modifier isChecked = isChecked,
.semantics(mergeDescendants = true) { contentDescription = "AcceptPoliciesToggle",
testTag = "AcceptPoliciesToggle" onCheckedChange = onCheckedChange,
toggleableState = ToggleableState(isChecked) subContent = {
}
.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,
)
FlowRow( FlowRow(
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.padding(end = 16.dp)
.fillMaxWidth()
.wrapContentHeight(),
) { ) {
BitwardenClickableText( BitwardenClickableText(
label = stringResource(id = R.string.terms_of_service), label = stringResource(id = R.string.terms_of_service),
@ -357,6 +320,6 @@ private fun TermsAndPrivacySwitch(
innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp), innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp),
) )
} }
} },
} )
} }

View file

@ -2,8 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.startregistration
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.Image 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.customActions
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag 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.AnnotatedString
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign 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.dropdown.EnvironmentSelector
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField 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.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.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
@ -277,7 +270,9 @@ private fun StartRegistrationContent(
isChecked = isReceiveMarketingEmailsToggled, isChecked = isReceiveMarketingEmailsToggled,
onCheckedChange = handler.onReceiveMarketingEmailsToggle, onCheckedChange = handler.onReceiveMarketingEmailsToggle,
onUnsubscribeClick = handler.onUnsubscribeMarketingEmailsClick, onUnsubscribeClick = handler.onUnsubscribeMarketingEmailsClick,
modifier = Modifier.standardHorizontalMargin(), modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
} }
@ -361,7 +356,6 @@ private fun TermsAndPrivacyText(
} }
} }
@Suppress("LongMethod")
@Composable @Composable
private fun ReceiveMarketingEmailsSwitch( private fun ReceiveMarketingEmailsSwitch(
isChecked: Boolean, isChecked: Boolean,
@ -373,7 +367,9 @@ private fun ReceiveMarketingEmailsSwitch(
@Suppress("MaxLineLength") @Suppress("MaxLineLength")
val annotatedLinkString = createClickableAnnotatedString( 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( highlights = listOf(
ClickableTextHighlight( ClickableTextHighlight(
textToHighlight = unsubscribeString, textToHighlight = unsubscribeString,
@ -381,13 +377,9 @@ private fun ReceiveMarketingEmailsSwitch(
), ),
), ),
) )
Row( BitwardenSwitch(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
.semantics(mergeDescendants = true) { .semantics(mergeDescendants = true) {
testTag = "ReceiveMarketingEmailsToggle"
toggleableState = ToggleableState(isChecked)
customActions = listOf( customActions = listOf(
CustomAccessibilityAction( CustomAccessibilityAction(
label = unsubscribeString, label = unsubscribeString,
@ -397,30 +389,12 @@ private fun ReceiveMarketingEmailsSwitch(
}, },
), ),
) )
} },
.clickable( label = annotatedLinkString,
interactionSource = remember { MutableInteractionSource() }, isChecked = isChecked,
indication = ripple( onCheckedChange = onCheckedChange,
color = BitwardenTheme.colorScheme.background.pressed, contentDescription = "ReceiveMarketingEmailsToggle",
),
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,
)
}
} }
@PreviewScreenSizes @PreviewScreenSizes

View file

@ -22,15 +22,17 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview 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.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.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors import com.x8bit.bitwarden.ui.platform.components.toggle.color.bitwardenSwitchColors
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme 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 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 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 * in between the [label] and the toggle. This lambda extends [RowScope], allowing flexibility in
* defining the layout of the actions. * defining the layout of the actions.
*/ */
@Suppress("LongMethod")
@Composable @Composable
fun BitwardenSwitch( fun BitwardenSwitch(
label: String, label: String,
@ -57,6 +58,154 @@ fun BitwardenSwitch(
readOnly: Boolean = false, readOnly: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
actions: (@Composable RowScope.() -> Unit)? = null, 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -95,17 +244,7 @@ fun BitwardenSwitch(
}, },
modifier = Modifier.testTag(tag = "SwitchText"), modifier = Modifier.testTag(tag = "SwitchText"),
) )
description?.let { subContent()
Text(
text = it,
style = BitwardenTheme.typography.bodyMedium,
color = if (enabled) {
BitwardenTheme.colorScheme.text.secondary
} else {
BitwardenTheme.colorScheme.filledButton.foregroundDisabled
},
)
}
} }
actions actions
@ -131,6 +270,7 @@ private fun BitwardenSwitch_preview() {
Column { Column {
BitwardenSwitch( BitwardenSwitch(
label = "Label", label = "Label",
description = "description",
isChecked = true, isChecked = true,
onCheckedChange = {}, onCheckedChange = {},
) )
@ -141,6 +281,7 @@ private fun BitwardenSwitch_preview() {
) )
BitwardenSwitch( BitwardenSwitch(
label = "Label", label = "Label",
description = "description",
isChecked = true, isChecked = true,
onCheckedChange = {}, onCheckedChange = {},
actions = { actions = {