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 ed3a56cb90
commit afd7fc51c4
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.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.calculateStartPadding
@ -25,14 +24,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset 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.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
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.LocalLayoutDirection 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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.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.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost
@ -189,12 +188,10 @@ private fun HomeScaffold(
onAccountSwitch = { onAccountSwitch = {
state.eventSink(HomeEvent.SwitchToAccount(it)) state.eventSink(HomeEvent.SwitchToAccount(it))
}, },
onCreateSpace = onCreateSpaceClick,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
displayFilters = state.displayRoomListFilters, displayFilters = state.displayRoomListFilters,
filtersState = roomListState.filtersState, filtersState = roomListState.filtersState,
spaceFiltersState = roomListState.spaceFiltersState, spaceFiltersState = roomListState.spaceFiltersState,
canCreateSpaces = state.homeSpacesState.canCreateSpaces,
canReportBug = state.canReportBug, canReportBug = state.canReportBug,
modifier = Modifier.hazeEffect( modifier = Modifier.hazeEffect(
state = hazeState, state = hazeState,
@ -226,12 +223,21 @@ private fun HomeScaffold(
state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item)) state.eventSink(HomeEvent.SelectHomeNavigationBarItem(item))
} }
}, },
floatingActionButton = { floatingActionButton = when (state.currentHomeNavigationBarItem) {
when (state.currentHomeNavigationBarItem) { HomeNavigationBarItem.Chats -> {
HomeNavigationBarItem.Chats -> HomeFloatingActionButton(onStartChatClick, CommonStrings.action_create_a_room) {
HomeNavigationBarItem.Spaces -> HomeFloatingActionButton(onCreateSpaceClick, CommonStrings.action_create_space) 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 @Composable
private fun HomeBottomBar( private fun HomeBottomBar(
currentHomeNavigationBarItem: HomeNavigationBarItem, currentHomeNavigationBarItem: HomeNavigationBarItem,
onItemClick: (HomeNavigationBarItem) -> Unit, onItemClick: (HomeNavigationBarItem) -> Unit,
floatingActionButton: @Composable () -> Unit, floatingActionButton: (@Composable () -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
HorizontalFloatingToolbar( HorizontalFloatingToolbar(
expanded = true,
floatingActionButton = floatingActionButton, floatingActionButton = floatingActionButton,
modifier = modifier modifier = modifier
.padding(bottom = ScreenOffset) .padding(bottom = ScreenOffset)
.zIndex(1f), .zIndex(1f),
) { ) {
HomeNavigationBarItem.entries.forEach { item -> HomeNavigationBarItem.entries.forEachIndexed { index, item ->
val isSelected = currentHomeNavigationBarItem == item if (index > 0) {
IconButton( HorizontalFloatingToolbarSeparator()
onClick = { onItemClick(item) },
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = item.icon(isSelected),
contentDescription = stringResource(item.labelRes),
)
}
} }
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, onMenuActionClick: (RoomListMenuAction) -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onAccountSwitch: (SessionId) -> Unit, onAccountSwitch: (SessionId) -> Unit,
onCreateSpace: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
canCreateSpaces: Boolean,
canReportBug: Boolean, canReportBug: Boolean,
displayFilters: Boolean, displayFilters: Boolean,
filtersState: RoomListFiltersState, filtersState: RoomListFiltersState,
@ -346,8 +344,6 @@ internal fun HomeTopBarPreview() = ElementPreview {
onOpenSettings = {}, onOpenSettings = {},
onAccountSwitch = {}, onAccountSwitch = {},
onToggleSearch = {}, onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true, canReportBug = true,
displayFilters = true, displayFilters = true,
filtersState = aRoomListFiltersState(), filtersState = aRoomListFiltersState(),
@ -392,8 +388,6 @@ internal fun HomeTopBarSpacesPreview() = ElementPreview {
onOpenSettings = {}, onOpenSettings = {},
onAccountSwitch = {}, onAccountSwitch = {},
onToggleSearch = {}, onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true, canReportBug = true,
displayFilters = false, displayFilters = false,
filtersState = aRoomListFiltersState(), filtersState = aRoomListFiltersState(),
@ -415,8 +409,6 @@ internal fun HomeTopBarWithIndicatorPreview() = ElementPreview {
onOpenSettings = {}, onOpenSettings = {},
onAccountSwitch = {}, onAccountSwitch = {},
onToggleSearch = {}, onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true, canReportBug = true,
displayFilters = true, displayFilters = true,
filtersState = aRoomListFiltersState(), filtersState = aRoomListFiltersState(),
@ -438,8 +430,6 @@ internal fun HomeTopBarMultiAccountPreview() = ElementPreview {
onOpenSettings = {}, onOpenSettings = {},
onAccountSwitch = {}, onAccountSwitch = {},
onToggleSearch = {}, onToggleSearch = {},
onCreateSpace = {},
canCreateSpaces = true,
canReportBug = true, canReportBug = true,
displayFilters = true, displayFilters = true,
filtersState = aRoomListFiltersState(), 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 = { },
)
}
}
}