Simplify the BitwardenExpandableFloatingActionButton (#5989)

This commit is contained in:
David Perez 2025-10-08 13:31:49 -05:00 committed by GitHub
parent 3a4f1d719f
commit 572d3357ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 77 additions and 128 deletions

View File

@ -50,7 +50,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.itemlisting.model.VaultDropdownMenuAction
import com.bitwarden.authenticator.ui.authenticator.feature.model.SharedCodesDisplayState import com.bitwarden.authenticator.ui.authenticator.feature.model.SharedCodesDisplayState
import com.bitwarden.authenticator.ui.authenticator.feature.model.VerificationCodeDisplayItem 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.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
import com.bitwarden.ui.platform.components.fab.BitwardenExpandableFloatingActionButton 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.header.BitwardenListHeaderText
import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.icon.model.IconData
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
@ -188,25 +188,25 @@ fun ItemListingScreen(
BitwardenExpandableFloatingActionButton( BitwardenExpandableFloatingActionButton(
modifier = Modifier.testTag("AddItemButton"), modifier = Modifier.testTag("AddItemButton"),
items = persistentListOf( items = persistentListOf(
ItemListingExpandableFabAction.ScanQrCode( ExpandableFabOption(
label = BitwardenString.scan_a_qr_code.asText(), label = BitwardenString.scan_a_qr_code.asText(),
icon = IconData.Local( icon = IconData.Local(
iconRes = BitwardenDrawable.ic_camera_small, iconRes = BitwardenDrawable.ic_camera_small,
contentDescription = BitwardenString.scan_a_qr_code.asText(), contentDescription = BitwardenString.scan_a_qr_code.asText(),
testTag = "ScanQRCodeButton", testTag = "ScanQRCodeButton",
), ),
onScanQrCodeClick = remember(viewModel) { onFabOptionClick = remember(viewModel) {
{ launcher.launch(Manifest.permission.CAMERA) } { launcher.launch(Manifest.permission.CAMERA) }
}, },
), ),
ItemListingExpandableFabAction.EnterSetupKey( ExpandableFabOption(
label = BitwardenString.enter_key_manually.asText(), label = BitwardenString.enter_key_manually.asText(),
icon = IconData.Local( icon = IconData.Local(
iconRes = BitwardenDrawable.ic_lock_encrypted_small, iconRes = BitwardenDrawable.ic_lock_encrypted_small,
contentDescription = BitwardenString.enter_key_manually.asText(), contentDescription = BitwardenString.enter_key_manually.asText(),
testTag = "EnterSetupKeyButton", testTag = "EnterSetupKeyButton",
), ),
onEnterSetupKeyClick = remember(viewModel) { onFabOptionClick = remember(viewModel) {
{ viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick) } { viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick) }
}, },
), ),

View File

@ -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,
)
}

View File

@ -17,16 +17,17 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.nullableTestTag 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.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.theme.BitwardenTheme import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.Text 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 items [ExpandableFabOption] buttons displayed when the FAB is expanded.
* @param label [Text] displayed when the FAB is expanded. * @param label [Text] displayed when the FAB is expanded.
* @param modifier The modifier for this composable. * @param modifier The modifier for this composable.
* @param expandableFabState [ExpandableFabIcon] displayed in the FAB. * @param initialIsExpanded The initial state of the [ExpandableFabIcon] displayed in the FAB.
* @param onStateChange Lambda invoked when the FAB expanded state changes. */
@Composable
fun BitwardenExpandableFloatingActionButton(
expandableFabIcon: ExpandableFabIcon,
items: ImmutableList<ExpandableFabOption>,
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") @Suppress("LongMethod")
@Composable @Composable
fun <T : ExpandableFabOption> BitwardenExpandableFloatingActionButton( fun BitwardenExpandableFloatingActionButton(
expandableFabIcon: ExpandableFabIcon, expandableFabIcon: ExpandableFabIcon,
items: ImmutableList<T>, items: ImmutableList<ExpandableFabOption>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
label: Text? = null, label: Text? = null,
expandableFabState: MutableState<ExpandableFabState> = rememberExpandableFabState(), isExpanded: Boolean,
onStateChange: (expandableFabState: ExpandableFabState) -> Unit = { }, onIsExpandedChange: (isExpanded: Boolean) -> Unit,
) { ) {
Column( Column(
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
@ -58,14 +87,14 @@ fun <T : ExpandableFabOption> BitwardenExpandableFloatingActionButton(
modifier = modifier, modifier = modifier,
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = expandableFabState.value.isExpanded(), visible = isExpanded,
label = "display_fab_options_animation", label = "display_fab_options_animation",
modifier = Modifier.weight(weight = 1f), modifier = Modifier.weight(weight = 1f),
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.clickable(interactionSource = null, indication = null) { .clickable(interactionSource = null, indication = null) {
expandableFabState.value = ExpandableFabState.Collapsed onIsExpandedChange(false)
} }
.fillMaxSize(), .fillMaxSize(),
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
@ -78,8 +107,7 @@ fun <T : ExpandableFabOption> BitwardenExpandableFloatingActionButton(
items(items) { expandableFabOption -> items(items) { expandableFabOption ->
ExpandableFabOption( ExpandableFabOption(
onFabOptionClick = { onFabOptionClick = {
expandableFabState.value = expandableFabState.value.toggleValue() onIsExpandedChange(!isExpanded)
onStateChange(expandableFabState.value)
expandableFabOption.onFabOptionClick() expandableFabOption.onFabOptionClick()
}, },
expandableFabOption = expandableFabOption, expandableFabOption = expandableFabOption,
@ -89,23 +117,12 @@ fun <T : ExpandableFabOption> BitwardenExpandableFloatingActionButton(
} }
val rotation by animateFloatAsState( val rotation by animateFloatAsState(
targetValue = if (expandableFabState.value.isExpanded()) { targetValue = if (isExpanded) expandableFabIcon.iconRotation else 0f,
expandableFabIcon.iconRotation ?: 0f
} else {
0f
},
label = "add_item_rotation", label = "add_item_rotation",
) )
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { onClick = { onIsExpandedChange(!isExpanded) },
expandableFabState.value = expandableFabState.value.toggleValue() expanded = if (label != null) !isExpanded else false,
onStateChange(expandableFabState.value)
},
expanded = if (label != null) {
!expandableFabState.value.isExpanded()
} else {
false
},
containerColor = BitwardenTheme.colorScheme.filledButton.background, containerColor = BitwardenTheme.colorScheme.filledButton.background,
contentColor = BitwardenTheme.colorScheme.filledButton.foreground, contentColor = BitwardenTheme.colorScheme.filledButton.foreground,
shape = BitwardenTheme.shapes.fab, shape = BitwardenTheme.shapes.fab,
@ -131,9 +148,9 @@ fun <T : ExpandableFabOption> BitwardenExpandableFloatingActionButton(
} }
@Composable @Composable
private fun <T : ExpandableFabOption> ExpandableFabOption( private fun ExpandableFabOption(
expandableFabOption: T, expandableFabOption: ExpandableFabOption,
onFabOptionClick: (option: T) -> Unit, onFabOptionClick: (option: ExpandableFabOption) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
SmallFloatingActionButton( SmallFloatingActionButton(
@ -163,53 +180,3 @@ private fun <T : ExpandableFabOption> ExpandableFabOption(
} }
} }
} }
@Composable
private fun rememberExpandableFabState(): MutableState<ExpandableFabState> =
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()
}

View File

@ -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,
)

View File

@ -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,
)