Merge pull request #4940 from element-hq/feature/bma/navigationBar

Navigation bar component
This commit is contained in:
Benoit Marty 2025-06-26 13:30:21 +02:00 committed by GitHub
commit aabc4965c5
9 changed files with 257 additions and 12 deletions

View file

@ -17,7 +17,7 @@ import androidx.compose.runtime.Composable
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.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
@ -36,18 +36,21 @@ private const val MAX_COUNT_STRING = "+$MAX_COUNT"
* @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING]. * @param count The number to display. If the number is greater than [MAX_COUNT], the counter will display [MAX_COUNT_STRING].
* If the number is less than 1, the counter will not be displayed. * If the number is less than 1, the counter will not be displayed.
* @param modifier The modifier to apply to this layout. * @param modifier The modifier to apply to this layout.
* @param textStyle The style to apply to the text inside the counter.
* @param isCritical If true, the counter will use a critical color scheme, otherwise it will use an accent color scheme.
*/ */
@Composable @Composable
fun CounterAtom( fun CounterAtom(
count: Int, count: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
textStyle: TextStyle = CounterAtomDefaults.textStyle,
isCritical: Boolean = false,
) { ) {
if (count < 1) return if (count < 1) return
val countAsText = when (count) { val countAsText = when (count) {
in 0..MAX_COUNT -> count.toString() in 0..MAX_COUNT -> count.toString()
else -> MAX_COUNT_STRING else -> MAX_COUNT_STRING
} }
val textStyle = ElementTheme.typography.fontBodyMdMedium
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
// Measure the maximum count string size // Measure the maximum count string size
val textLayoutResult = textMeasurer.measure( val textLayoutResult = textMeasurer.measure(
@ -58,19 +61,30 @@ fun CounterAtom(
val squareSize = maxOf(textSize.width, textSize.height) val squareSize = maxOf(textSize.width, textSize.height)
Box( Box(
modifier = modifier modifier = modifier
.size(squareSize.toDp() + 1.dp) .size(squareSize.toDp() + 1.dp)
.clip(CircleShape) .clip(CircleShape)
.background(ElementTheme.colors.iconSuccessPrimary) .background(
if (isCritical) {
ElementTheme.colors.iconCriticalPrimary
} else {
ElementTheme.colors.iconAccentPrimary
}
)
) { ) {
Text( Text(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
text = countAsText, text = countAsText,
style = textStyle, style = textStyle,
color = Color.White, color = ElementTheme.colors.textOnSolidPrimary,
) )
} }
} }
object CounterAtomDefaults {
val textStyle: TextStyle
@Composable get() = ElementTheme.typography.fontBodyMdMedium
}
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun CounterAtomPreview() = ElementPreview { internal fun CounterAtomPreview() = ElementPreview {
@ -79,5 +93,6 @@ internal fun CounterAtomPreview() = ElementPreview {
CounterAtom(count = 4) CounterAtom(count = 4)
CounterAtom(count = 99) CounterAtom(count = 99)
CounterAtom(count = 100) CounterAtom(count = 100)
CounterAtom(count = 4, isCritical = true)
} }
} }

View file

@ -0,0 +1,111 @@
/*
* Copyright 2025 New Vector 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.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
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.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
@Composable
fun NavigationBar(
modifier: Modifier = Modifier,
containerColor: Color = ElementNavigationBarDefaults.containerColor,
contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor),
tonalElevation: Dp = ElementNavigationBarDefaults.tonalElevation,
windowInsets: WindowInsets = ElementNavigationBarDefaults.windowInsets,
content: @Composable RowScope.() -> Unit
) {
androidx.compose.material3.NavigationBar(
modifier = modifier,
containerColor = containerColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
windowInsets = windowInsets,
content = content
)
}
object ElementNavigationBarDefaults {
val containerColor: Color
@Composable get() = if (ElementTheme.isLightTheme) {
ElementTheme.colors.bgSubtlePrimary
} else {
ElementTheme.colors.textOnSolidPrimary
}
val tonalElevation: Dp = NavigationBarDefaults.Elevation
val windowInsets: WindowInsets
@Composable get() = NavigationBarDefaults.windowInsets
}
@Preview(group = PreviewGroup.AppBars)
@Composable
internal fun NavigationBarPreview() = ElementThemedPreview {
NavigationBar {
NavigationBarItem(
icon = {
NavigationBarIcon(
imageVector = CompoundIcons.ChatSolid(),
count = 5,
isCritical = false,
)
},
label = {
NavigationBarText(
text = "Chats"
)
},
selected = true,
onClick = {},
)
NavigationBarItem(
icon = {
NavigationBarIcon(
imageVector = CompoundIcons.ChatSolid(),
count = 5,
isCritical = true,
)
},
label = {
NavigationBarText(
text = "Teams"
)
},
selected = false,
onClick = {},
)
NavigationBarItem(
icon = {
NavigationBarIcon(
imageVector = CompoundIcons.ChatSolid(),
count = 0,
isCritical = false,
)
},
label = {
NavigationBarText(
text = "Other"
)
},
selected = false,
onClick = {},
)
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2025 New Vector 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.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom
@Composable
fun NavigationBarIcon(
imageVector: ImageVector,
modifier: Modifier = Modifier,
count: Int = 0,
isCritical: Boolean = false,
) {
Box(modifier) {
Icon(
imageVector = imageVector,
contentDescription = null,
)
CounterAtom(
modifier = Modifier.offset(11.dp, (-11).dp),
textStyle = ElementTheme.typography.fontBodyXsMedium,
count = count,
isCritical = isCritical,
)
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2025 New Vector 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.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemColors
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import io.element.android.compound.theme.ElementTheme
@Composable
fun RowScope.NavigationBarItem(
selected: Boolean,
onClick: () -> Unit,
icon: @Composable () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: @Composable (() -> Unit)? = null,
alwaysShowLabel: Boolean = true,
colors: NavigationBarItemColors = ElementNavigationBarItemDefaults.colors(),
interactionSource: MutableInteractionSource? = null
) {
NavigationBarItem(
selected = selected,
onClick = onClick,
icon = icon,
modifier = modifier,
enabled = enabled,
label = label,
alwaysShowLabel = alwaysShowLabel,
colors = colors,
interactionSource = interactionSource,
)
}
object ElementNavigationBarItemDefaults {
@Composable
fun colors() = NavigationBarItemDefaults.colors().copy(
selectedIconColor = ElementTheme.colors.iconPrimary,
selectedTextColor = ElementTheme.colors.textPrimary,
unselectedIconColor = ElementTheme.colors.iconTertiary,
unselectedTextColor = ElementTheme.colors.textDisabled,
selectedIndicatorColor = Color.Companion.Transparent,
)
}

View file

@ -0,0 +1,24 @@
/*
* Copyright 2025 New Vector 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.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.compound.theme.ElementTheme
@Composable
fun NavigationBarText(
text: String,
modifier: Modifier = Modifier,
) {
Text(
modifier = modifier,
text = text,
style = ElementTheme.typography.fontBodySmMedium,
)
}

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8201ab8637db30a33b16a1336705f1777fff946832e9f10ac45987eaa44a20b4 oid sha256:3f40ee24252a856c4680dc0b62865e71108d54b326c81b651dcf9031c8d7ae95
size 42884 size 42835

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7a2a171ac0edac9cb734d418b7baf68dd294d9feb93f005cf445420a7538a8ed oid sha256:49f6848dbff23f8c906894ee852253c051807c942f968ba3856e361aeae5c2ca
size 7687 size 8683

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:56508c5827cfda4c672f924e3e9280659b729b772455c65dbae76ba7815e18dd oid sha256:d341142ffd67a41fee3b2f41146011caeee07b9ec3085d9c47131d1a5d68683a
size 7502 size 8002

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0554fae92f638dcb6f0701c36f67df8126402a74a8864daf8f821cd5d3ffd29c
size 16367