Create HorizontalFloatingToolbar wrapper in our components.

This commit is contained in:
Benoit Marty 2026-02-04 17:48:02 +01:00 committed by Benoit Marty
parent ff257164d3
commit 86c7d04176
3 changed files with 252 additions and 35 deletions

View file

@ -13,7 +13,6 @@ package io.element.android.features.home.impl
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
@ -25,14 +24,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.HorizontalFloatingToolbar
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalLayoutDirection
@ -65,6 +61,9 @@ import io.element.android.libraries.androidutils.throttler.FirstThrottler
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.FloatingActionButton
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbar
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarItem
import io.element.android.libraries.designsystem.theme.components.HorizontalFloatingToolbarSeparator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
@ -189,12 +188,10 @@ private fun HomeScaffold(
onAccountSwitch = {
state.eventSink(HomeEvent.SwitchToAccount(it))
},
onCreateSpace = onCreateSpaceClick,
scrollBehavior = scrollBehavior,
displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState,
canCreateSpaces = state.homeSpacesState.canCreateSpaces,
canReportBug = state.canReportBug,
modifier = Modifier.hazeEffect(
state = hazeState,
@ -226,12 +223,21 @@ private fun HomeScaffold(
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item))
}
},
floatingActionButton = {
when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_a_room)
HomeNavigationBarItem.Spaces -> HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space)
floatingActionButton = when (state.currentHomeNavigationBarItem) {
HomeNavigationBarItem.Chats -> {
{
HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_room)
}
}
}
HomeNavigationBarItem.Spaces -> if (state.homeSpacesState.canCreateSpaces) {
{
HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space)
}
} else {
// No FAB for spaces if we cannot create spaces
null
}
},
)
}
},
@ -311,33 +317,31 @@ private fun HomeFloatingActionButton(
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun HomeBottomBar(
currentHomeNavigationBarItem: HomeNavigationBarItem,
onItemClick: (HomeNavigationBarItem) -> Unit,
floatingActionButton: @Composable () -> Unit,
floatingActionButton: (@Composable () -> Unit)?,
modifier: Modifier = Modifier,
) {
HorizontalFloatingToolbar(
expanded = true,
floatingActionButton = floatingActionButton,
modifier = modifier
.padding(bottom = ScreenOffset)
.zIndex(1f),
) {
HomeNavigationBarItem.entries.forEach { item ->
val isSelected = currentHomeNavigationBarItem == item
IconButton(
onClick = { onItemClick(item) },
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = item.icon(isSelected),
contentDescription = stringResource(item.labelRes),
)
}
HomeNavigationBarItem.entries.forEachIndexed { index, item ->
if (index > 0) {
HorizontalFloatingToolbarSeparator()
}
val isSelected = currentHomeNavigationBarItem == item
HorizontalFloatingToolbarItem(
icon = item.icon(isSelected),
tooltipLabel = stringResource(item.labelRes),
isSelected = isSelected,
onClick = { onItemClick(item) },
)
}
}
}

View file

@ -87,9 +87,7 @@ fun HomeTopBar(
onMenuActionClick: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit,
onAccountSwitch: (SessionId) -> Unit,
onCreateSpace: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior,
canCreateSpaces: Boolean,
canReportBug: Boolean,
displayFilters: Boolean,
filtersState: RoomListFiltersState,
@ -346,8 +344,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
@ -392,8 +388,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = false,
filtersState = aRoomListFiltersState(),
@ -415,8 +409,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),
@ -438,8 +430,6 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
onOpenSettings = {},
onAccountSwitch = {},
onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true,
displayFilters = true,
filtersState = aRoomListFiltersState(),

View file

@ -0,0 +1,223 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.FloatingToolbarColors
import androidx.compose.material3.FloatingToolbarDefaults
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
import androidx.compose.material3.FloatingToolbarScrollBehavior
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom
import io.element.android.libraries.designsystem.components.tooltip.PlainTooltip
import io.element.android.libraries.designsystem.components.tooltip.TooltipBox
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
/**
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=4457-1136
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun HorizontalFloatingToolbar(
modifier: Modifier = Modifier,
expanded: Boolean = true,
floatingActionButton: (@Composable () -> Unit)? = null,
colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors().copy(
toolbarContainerColor = ElementTheme.colors.bgSubtleSecondary,
),
contentPadding: PaddingValues = PaddingValues(
vertical = 8.dp,
horizontal = 12.dp,
),
scrollBehavior: FloatingToolbarScrollBehavior? = null,
shape: Shape = FloatingToolbarDefaults.ContainerShape,
leadingContent: @Composable (RowScope.() -> Unit)? = null,
trailingContent: @Composable (RowScope.() -> Unit)? = null,
floatingActionButtonPosition: FloatingToolbarHorizontalFabPosition =
FloatingToolbarHorizontalFabPosition.End,
animationSpec: FiniteAnimationSpec<Float> = FloatingToolbarDefaults.animationSpec(),
expandedShadowElevation: Dp = 8.dp,
collapsedShadowElevation: Dp = if (floatingActionButton == null) {
FloatingToolbarDefaults.ContainerCollapsedElevation
} else {
FloatingToolbarDefaults.ContainerCollapsedElevationWithFab
},
content: @Composable RowScope.() -> Unit,
) {
if (floatingActionButton == null) {
androidx.compose.material3.HorizontalFloatingToolbar(
expanded = expanded,
modifier = modifier,
colors = colors,
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
shape = shape,
leadingContent = leadingContent,
trailingContent = trailingContent,
expandedShadowElevation = expandedShadowElevation,
collapsedShadowElevation = collapsedShadowElevation,
content = content,
)
} else {
androidx.compose.material3.HorizontalFloatingToolbar(
expanded = expanded,
floatingActionButton = floatingActionButton,
modifier = modifier,
colors = colors,
contentPadding = contentPadding,
scrollBehavior = scrollBehavior,
shape = shape,
floatingActionButtonPosition = floatingActionButtonPosition,
animationSpec = animationSpec,
expandedShadowElevation = expandedShadowElevation,
collapsedShadowElevation = collapsedShadowElevation,
content = content,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HorizontalFloatingToolbarItem(
icon: ImageVector,
tooltipLabel: String,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
counter: Int? = null,
forceRenderingTooltip: Boolean = false,
) {
TooltipBox(
positionProvider =
TooltipDefaults.rememberTooltipPositionProvider(
TooltipAnchorPosition.Above
),
tooltip = { PlainTooltip { Text(tooltipLabel) } },
state = rememberTooltipState(
initialIsVisible = forceRenderingTooltip,
),
modifier = modifier,
) {
val colors = if (isSelected) {
IconButtonDefaults.filledIconButtonColors().copy(
containerColor = ElementTheme.colors.bgCanvasDefault,
contentColor = ElementTheme.colors.iconPrimary,
)
} else {
IconButtonDefaults.filledIconButtonColors().copy(
containerColor = Color.Transparent,
contentColor = ElementTheme.colors.iconSecondary,
)
}
Box {
FilledIconButton(
modifier = Modifier.widthIn(min = 56.dp),
colors = colors,
onClick = onClick,
) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = icon,
contentDescription = tooltipLabel,
)
}
if (counter != null) {
CounterAtom(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 6.dp, end = 3.dp),
count = counter,
textStyle = ElementTheme.typography.fontBodyXsMedium,
)
}
}
}
}
@Composable
fun HorizontalFloatingToolbarSeparator(modifier: Modifier = Modifier) {
Spacer(modifier = modifier.width(16.dp))
}
@PreviewsDayNight
@Composable
internal fun HorizontalFloatingToolbarPreview() = ElementPreview {
ContentToPreview(
floatingActionButton = {
FloatingActionButton(
onClick = {},
) {
Icon(
imageVector = CompoundIcons.Plus(),
contentDescription = null,
)
}
}
)
}
@PreviewsDayNight
@Composable
internal fun HorizontalFloatingToolbarNoFabPreview() = ElementPreview {
ContentToPreview(
floatingActionButton = null,
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun ContentToPreview(
floatingActionButton: (@Composable () -> Unit)?,
) {
HorizontalFloatingToolbar(
modifier = Modifier.padding(28.dp),
floatingActionButton = floatingActionButton,
) {
listOf(
CompoundIcons.ChatSolid(),
CompoundIcons.Space(),
).forEachIndexed { index, icon ->
if (index > 0) {
HorizontalFloatingToolbarSeparator()
}
HorizontalFloatingToolbarItem(
icon = icon,
tooltipLabel = "Label",
isSelected = index == 0,
counter = if (index == 0) 6 else null,
forceRenderingTooltip = true,
onClick = { },
)
}
}
}