Merge pull request #4940 from element-hq/feature/bma/navigationBar
Navigation bar component
This commit is contained in:
commit
aabc4965c5
9 changed files with 257 additions and 12 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0554fae92f638dcb6f0701c36f67df8126402a74a8864daf8f821cd5d3ffd29c
|
||||||
|
size 16367
|
||||||
Loading…
Add table
Add a link
Reference in a new issue