From 572d3357ee5e27cb2471603678a70b70bd572a3f Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 8 Oct 2025 13:31:49 -0500 Subject: [PATCH] Simplify the BitwardenExpandableFloatingActionButton (#5989) --- .../feature/itemlisting/ItemListingScreen.kt | 12 +- .../model/ItemListingExpandableFabAction.kt | 42 ------ ...BitwardenExpandableFloatingActionButton.kt | 127 +++++++----------- .../components/fab/model/ExpandableFabIcon.kt | 11 ++ .../fab/model/ExpandableFabOption.kt | 13 ++ 5 files changed, 77 insertions(+), 128 deletions(-) delete mode 100644 authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt create mode 100644 ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabIcon.kt create mode 100644 ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabOption.kt diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt index d2f2d52b43..2193803c7a 100644 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt +++ b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/ItemListingScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingExpandableFabAction import com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.VaultDropdownMenuAction import com.bitwarden.authenticator.ui.authenticator.feature.model.SharedCodesDisplayState import com.bitwarden.authenticator.ui.authenticator.feature.model.VerificationCodeDisplayItem @@ -73,7 +72,8 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.bitwarden.ui.platform.components.fab.BitwardenExpandableFloatingActionButton -import com.bitwarden.ui.platform.components.fab.ExpandableFabIcon +import com.bitwarden.ui.platform.components.fab.model.ExpandableFabIcon +import com.bitwarden.ui.platform.components.fab.model.ExpandableFabOption import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold @@ -188,25 +188,25 @@ fun ItemListingScreen( BitwardenExpandableFloatingActionButton( modifier = Modifier.testTag("AddItemButton"), items = persistentListOf( - ItemListingExpandableFabAction.ScanQrCode( + ExpandableFabOption( label = BitwardenString.scan_a_qr_code.asText(), icon = IconData.Local( iconRes = BitwardenDrawable.ic_camera_small, contentDescription = BitwardenString.scan_a_qr_code.asText(), testTag = "ScanQRCodeButton", ), - onScanQrCodeClick = remember(viewModel) { + onFabOptionClick = remember(viewModel) { { launcher.launch(Manifest.permission.CAMERA) } }, ), - ItemListingExpandableFabAction.EnterSetupKey( + ExpandableFabOption( label = BitwardenString.enter_key_manually.asText(), icon = IconData.Local( iconRes = BitwardenDrawable.ic_lock_encrypted_small, contentDescription = BitwardenString.enter_key_manually.asText(), testTag = "EnterSetupKeyButton", ), - onEnterSetupKeyClick = remember(viewModel) { + onFabOptionClick = remember(viewModel) { { viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick) } }, ), diff --git a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt b/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt deleted file mode 100644 index 20d0598253..0000000000 --- a/authenticator/src/main/kotlin/com/bitwarden/authenticator/ui/authenticator/feature/itemlisting/model/ItemListingExpandableFabAction.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model - -import androidx.compose.material3.ExtendedFloatingActionButton -import com.bitwarden.ui.platform.components.fab.ExpandableFabOption -import com.bitwarden.ui.platform.components.icon.model.IconData -import com.bitwarden.ui.util.Text - -/** - * Models [ExpandableFabOption]s that can be triggered by the [ExtendedFloatingActionButton]. - */ -sealed class ItemListingExpandableFabAction( - label: Text, - icon: IconData.Local, - onFabOptionClick: () -> Unit, -) : ExpandableFabOption(label, icon, onFabOptionClick) { - - /** - * Indicates the Scan QR code button was clicked. - */ - class ScanQrCode( - label: Text, - icon: IconData.Local, - onScanQrCodeClick: () -> Unit, - ) : ItemListingExpandableFabAction( - label = label, - icon = icon, - onFabOptionClick = onScanQrCodeClick, - ) - - /** - * Indicates the Enter Key button was clicked. - */ - class EnterSetupKey( - label: Text, - icon: IconData.Local, - onEnterSetupKeyClick: () -> Unit, - ) : ItemListingExpandableFabAction( - label = label, - icon = icon, - onFabOptionClick = onEnterSetupKeyClick, - ) -} diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/BitwardenExpandableFloatingActionButton.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/BitwardenExpandableFloatingActionButton.kt index f9fd24cb6f..038b485274 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/BitwardenExpandableFloatingActionButton.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/BitwardenExpandableFloatingActionButton.kt @@ -17,16 +17,17 @@ import androidx.compose.material3.Icon import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState 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.draw.rotate import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.base.util.nullableTestTag -import com.bitwarden.ui.platform.components.icon.model.IconData +import com.bitwarden.ui.platform.components.fab.model.ExpandableFabIcon +import com.bitwarden.ui.platform.components.fab.model.ExpandableFabOption import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.util.Text @@ -39,18 +40,46 @@ import kotlinx.collections.immutable.ImmutableList * @param items [ExpandableFabOption] buttons displayed when the FAB is expanded. * @param label [Text] displayed when the FAB is expanded. * @param modifier The modifier for this composable. - * @param expandableFabState [ExpandableFabIcon] displayed in the FAB. - * @param onStateChange Lambda invoked when the FAB expanded state changes. + * @param initialIsExpanded The initial state of the [ExpandableFabIcon] displayed in the FAB. + */ +@Composable +fun BitwardenExpandableFloatingActionButton( + expandableFabIcon: ExpandableFabIcon, + items: ImmutableList, + modifier: Modifier = Modifier, + label: Text? = null, + initialIsExpanded: Boolean = false, +) { + var isExpanded by rememberSaveable { mutableStateOf(value = initialIsExpanded) } + BitwardenExpandableFloatingActionButton( + expandableFabIcon = expandableFabIcon, + items = items, + label = label, + isExpanded = isExpanded, + onIsExpandedChange = { isExpanded = it }, + modifier = modifier, + ) +} + +/** + * A FAB that expands, when clicked, to display a collection of options that can be clicked. + * + * @param expandableFabIcon The icon to display and how to display it. + * @param items [ExpandableFabOption] buttons displayed when the FAB is expanded. + * @param label [Text] displayed when the FAB is expanded. + * @param modifier The modifier for this composable. + * @param isExpanded whether the FAB is in the expanded state. + * @param onIsExpandedChange Lambda invoked when the FAB expanded state changes. */ @Suppress("LongMethod") @Composable -fun BitwardenExpandableFloatingActionButton( +fun BitwardenExpandableFloatingActionButton( expandableFabIcon: ExpandableFabIcon, - items: ImmutableList, + items: ImmutableList, modifier: Modifier = Modifier, label: Text? = null, - expandableFabState: MutableState = rememberExpandableFabState(), - onStateChange: (expandableFabState: ExpandableFabState) -> Unit = { }, + isExpanded: Boolean, + onIsExpandedChange: (isExpanded: Boolean) -> Unit, ) { Column( horizontalAlignment = Alignment.End, @@ -58,14 +87,14 @@ fun BitwardenExpandableFloatingActionButton( modifier = modifier, ) { AnimatedVisibility( - visible = expandableFabState.value.isExpanded(), + visible = isExpanded, label = "display_fab_options_animation", modifier = Modifier.weight(weight = 1f), ) { LazyColumn( modifier = Modifier .clickable(interactionSource = null, indication = null) { - expandableFabState.value = ExpandableFabState.Collapsed + onIsExpandedChange(false) } .fillMaxSize(), horizontalAlignment = Alignment.End, @@ -78,8 +107,7 @@ fun BitwardenExpandableFloatingActionButton( items(items) { expandableFabOption -> ExpandableFabOption( onFabOptionClick = { - expandableFabState.value = expandableFabState.value.toggleValue() - onStateChange(expandableFabState.value) + onIsExpandedChange(!isExpanded) expandableFabOption.onFabOptionClick() }, expandableFabOption = expandableFabOption, @@ -89,23 +117,12 @@ fun BitwardenExpandableFloatingActionButton( } val rotation by animateFloatAsState( - targetValue = if (expandableFabState.value.isExpanded()) { - expandableFabIcon.iconRotation ?: 0f - } else { - 0f - }, + targetValue = if (isExpanded) expandableFabIcon.iconRotation else 0f, label = "add_item_rotation", ) ExtendedFloatingActionButton( - onClick = { - expandableFabState.value = expandableFabState.value.toggleValue() - onStateChange(expandableFabState.value) - }, - expanded = if (label != null) { - !expandableFabState.value.isExpanded() - } else { - false - }, + onClick = { onIsExpandedChange(!isExpanded) }, + expanded = if (label != null) !isExpanded else false, containerColor = BitwardenTheme.colorScheme.filledButton.background, contentColor = BitwardenTheme.colorScheme.filledButton.foreground, shape = BitwardenTheme.shapes.fab, @@ -131,9 +148,9 @@ fun BitwardenExpandableFloatingActionButton( } @Composable -private fun ExpandableFabOption( - expandableFabOption: T, - onFabOptionClick: (option: T) -> Unit, +private fun ExpandableFabOption( + expandableFabOption: ExpandableFabOption, + onFabOptionClick: (option: ExpandableFabOption) -> Unit, modifier: Modifier = Modifier, ) { SmallFloatingActionButton( @@ -163,53 +180,3 @@ private fun ExpandableFabOption( } } } - -@Composable -private fun rememberExpandableFabState(): MutableState = - remember { mutableStateOf(ExpandableFabState.Collapsed) } - -/** - * Represents options displayed when the FAB is expanded. - */ -abstract class ExpandableFabOption( - val label: Text, - val icon: IconData.Local, - val onFabOptionClick: () -> Unit, -) - -/** - * Models data for an expandable FAB icon. - */ -data class ExpandableFabIcon( - val icon: IconData.Local, - val iconRotation: Float?, -) - -/** - * Models the state of the expandable FAB. - */ -sealed class ExpandableFabState { - /** - * Indicates if the FAB is expanded. - */ - fun isExpanded(): Boolean = this == Expanded - - /** - * Invert the state of the FAB. - */ - fun toggleValue(): ExpandableFabState = if (isExpanded()) { - Collapsed - } else { - Expanded - } - - /** - * Indicates the FAB is collapsed. - */ - data object Collapsed : ExpandableFabState() - - /** - * Indicates the FAB is expanded. - */ - data object Expanded : ExpandableFabState() -} diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabIcon.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabIcon.kt new file mode 100644 index 0000000000..cc1db8f864 --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabIcon.kt @@ -0,0 +1,11 @@ +package com.bitwarden.ui.platform.components.fab.model + +import com.bitwarden.ui.platform.components.icon.model.IconData + +/** + * Models data for an expandable FAB icon. + */ +data class ExpandableFabIcon( + val icon: IconData.Local, + val iconRotation: Float, +) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabOption.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabOption.kt new file mode 100644 index 0000000000..999bf69392 --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/fab/model/ExpandableFabOption.kt @@ -0,0 +1,13 @@ +package com.bitwarden.ui.platform.components.fab.model + +import com.bitwarden.ui.platform.components.icon.model.IconData +import com.bitwarden.ui.util.Text + +/** + * Represents options displayed when the FAB is expanded. + */ +data class ExpandableFabOption( + val label: Text, + val icon: IconData.Local, + val onFabOptionClick: () -> Unit, +)