tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit e6b00e38d95a3db7b7356e644d6adab8c71a5db0
parent a53b0fc697af7360e5b4ae7c91a88c587583fca4
Author: Julie De Lorenzo <jdelorenzo@mozilla.com>
Date:   Mon,  8 Dec 2025 22:55:30 +0000

Bug 1994783:  Add a reusable badge component r=android-reviewers,007

Differential Revision: https://phabricator.services.mozilla.com/D270738

Diffstat:
Mmobile/android/android-components/components/compose/base/build.gradle | 1+
Amobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/badge/BadgedIcon.kt | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amobile/android/android-components/components/compose/base/src/test/java/mozilla/components/compose/base/badge/BadgedIconTest.kt | 326+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/LibraryMenuItem.kt | 26++++++++++----------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
5 files changed, 721 insertions(+), 33 deletions(-)

diff --git a/mobile/android/android-components/components/compose/base/build.gradle b/mobile/android/android-components/components/compose/base/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation libs.androidx.navigation.compose debugImplementation libs.androidx.compose.ui.tooling + testImplementation libs.androidx.compose.ui.test.manifest testImplementation project(':components:support-test') diff --git a/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/badge/BadgedIcon.kt b/mobile/android/android-components/components/compose/base/src/main/java/mozilla/components/compose/base/badge/BadgedIcon.kt @@ -0,0 +1,319 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.compose.base.badge + +import androidx.annotation.IntDef +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.material3.Badge +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import mozilla.components.compose.base.modifier.thenConditional +import mozilla.components.compose.base.theme.AcornTheme +import mozilla.components.compose.base.theme.acornPrivateColorScheme +import mozilla.components.compose.base.theme.information +import mozilla.components.compose.base.utils.toLocaleString +import mozilla.components.ui.icons.R as iconsR + +/** + * Class representing a [Badge] size. + * BADGE_SIZE_SMALL is 8x8dp. + * BADGE_SIZE_LARGE is 16x16dp or up to 16x32dp. + */ +@Retention(AnnotationRetention.SOURCE) +@IntDef(BADGE_SIZE_SMALL, BADGE_SIZE_LARGE) +annotation class BadgeSize + +/** + * Small badge size, 8x8dp. + */ +const val BADGE_SIZE_SMALL = 0 + +/** + * Large badge size, 16x16dp or up to 16x32dp. + */ +const val BADGE_SIZE_LARGE = 1 + +/** + * Test tag to find the badge. + */ +const val BADGE_TEST_TAG = "badge" + +private const val MAX_BADGE_COUNT = 99 +private const val MAX_BADGE_COUNT_EXCEEDED = "\u221e" +private val SYMBOL_VERTICAL_OFFSET = (-0.5).dp +private val SMALL_BADGE_OFFSET = IntOffset(1, (-1)) +private val LARGE_BADGE_OFFSET = IntOffset(8, (-4)) + +/** + * Badged icon. + * The badge may be small (8x8 Dp) or large (with up to two numeric digits (99) + * + * @param painter [Painter] representing the icon + * @param isHighlighted whether or not the button is highlighted. No badge will be shown + * if highlighted is false. + * @param modifier [Modifier] + * @param size [BadgeSize], defaults to BADGE_SIZE_SMALL + * @param notificationCount Int value representing count drawn inside badge. Defaults to 0 + * @param contentDescription String content description + * @param containerColor [Color] of the badge's container color + * @param tint [Color] of the icon's tint + * @param badgeContentColor [Color] of the badge's content color (text color) + */ +@Composable +fun BadgedIcon( + painter: Painter, + isHighlighted: Boolean, + modifier: Modifier = Modifier, + @BadgeSize size: Int = BADGE_SIZE_SMALL, + notificationCount: Int = 0, + contentDescription: String? = null, + containerColor: Color = MaterialTheme.colorScheme.information, + tint: Color = MaterialTheme.colorScheme.onSurface, + badgeContentColor: Color = MaterialTheme.colorScheme.onError, +) { + BadgedBox( + isHighlighted = isHighlighted, + size = size, + modifier = modifier, + notificationCount = notificationCount, + containerColor = containerColor, + badgeContentColor = badgeContentColor, + ) { + Icon( + painter = painter, + contentDescription = contentDescription, + tint = tint, + ) + } +} + +@Composable +@PreviewLightDark +@PreviewNumericSystems +private fun BadgedIconPreview( + @PreviewParameter(BadgeProvider::class) config: BadgeData, +) { + AcornTheme { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(16.dp), + verticalArrangement = spacedBy(16.dp), + ) { + BadgedIcon( + isHighlighted = config.isHighlighted, + painter = painterResource(config.icon), + size = config.size, + notificationCount = config.notificationCount, + ) + } + } +} + +@Composable +@Preview +private fun BadgedIconPreviewPrivate( + @PreviewParameter(BadgeProvider::class) config: BadgeData, +) { + AcornTheme(colorScheme = acornPrivateColorScheme()) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(16.dp), + verticalArrangement = spacedBy(16.dp), + ) { + BadgedIcon( + isHighlighted = config.isHighlighted, + painter = painterResource(config.icon), + size = config.size, + notificationCount = config.notificationCount, + ) + } + } +} + +/** + * Converts a numeric count to a Locale-appropriate string, or ∞ if max count has + * been exceeded. + * + * Treatment aligns with [TabCounter] and [TabCounterButton] composables. + */ +@Composable +private fun convertNotificationCountToLabel(notificationCount: Int): String { + return if (notificationCount <= MAX_BADGE_COUNT) { + notificationCount.toLocaleString() + } else { + MAX_BADGE_COUNT_EXCEEDED + } +} + +/** + * Content to be shown inside the badge. + * Content is only rendered for the large [BadgeSize]. + * @param size [BadgeSize] + * @param notificationCount Int representing count displayed in badge. Defaults to 0. + * @return string content, or null if no content is to be drawn. + */ +@Composable +private fun badgeContent( + @BadgeSize size: Int, + notificationCount: Int = 0, +): @Composable (RowScope.() -> Unit)? { + val notificationLabel = convertNotificationCountToLabel(notificationCount) + return when (size) { + BADGE_SIZE_LARGE -> { + { + Text( + // Adds a small offset to center the ∞ symbol + modifier = Modifier + .thenConditional( + Modifier.offset(y = SYMBOL_VERTICAL_OFFSET), + predicate = { notificationLabel == MAX_BADGE_COUNT_EXCEEDED }, + ), + text = notificationLabel, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onError, + maxLines = 1, + ) + } + } + else -> { + null + } + } +} + +/** + * Box to house the badged icon. The [BadgedBox] provided by Material 3 does not work for our + * purposes because the Mozilla badge sizing is different. + * + * @param isHighlighted Boolean value representing whether or not the icon is highlighted. The badge + * is only drawn if the icon is highlighted. + * @param size [BadgeSize] + * @param modifier [Modifier] + * @param notificationCount Int representing the count shown in the badge content + * @param containerColor [Color] of the badge's container color + * @param badgeContentColor [Color] of the badge's content color (text color) + * @param content The content rendered inside the badge + */ +@Composable +private fun BadgedBox( + isHighlighted: Boolean, + @BadgeSize size: Int, + modifier: Modifier, + notificationCount: Int = 0, + containerColor: Color = MaterialTheme.colorScheme.information, + badgeContentColor: Color = MaterialTheme.colorScheme.onError, + content: @Composable BoxScope.() -> Unit, +) { + val offset: IntOffset by remember(size) { + derivedStateOf { + if (size == BADGE_SIZE_SMALL) { + SMALL_BADGE_OFFSET + } else { + LARGE_BADGE_OFFSET + } + } + } + + Box(modifier = modifier) { + content() + if (isHighlighted) { + Badge( + modifier = Modifier + .thenConditional(Modifier.requiredSize(8.dp), { size == BADGE_SIZE_SMALL }) + .thenConditional(Modifier.requiredHeight(16.dp), predicate = { size == BADGE_SIZE_LARGE }) + .align(alignment = Alignment.TopEnd) + .offset { IntOffset(x = offset.x.dp.roundToPx(), y = offset.y.dp.roundToPx()) } + .testTag(BADGE_TEST_TAG), + containerColor = containerColor, + contentColor = badgeContentColor, + content = badgeContent(size = size, notificationCount = notificationCount), + ) + } + } +} + +private data class BadgeData( + val isHighlighted: Boolean = false, + val icon: Int = iconsR.drawable.mozac_ic_download_24, + val size: Int = BADGE_SIZE_SMALL, + val notificationCount: Int = 0, +) +private class BadgeProvider : PreviewParameterProvider<BadgeData> { + override val values = sequenceOf( + // small, not highlighted + BadgeData( + isHighlighted = false, + size = BADGE_SIZE_SMALL, + ), + // large, not highlighted + BadgeData( + isHighlighted = false, + size = BADGE_SIZE_LARGE, + ), + // small, highlighted + BadgeData( + isHighlighted = true, + size = BADGE_SIZE_SMALL, + ), + // large, highlighted single digit + BadgeData( + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 3, + ), + // large, highlighted double digit + BadgeData( + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 99, + ), + // large, highlighted triple digit + BadgeData( + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 999, + ), + ) +} + +/** + * An annotation class representing Locales that represent different + * numeric systems. If this is useful in other contexts it may be + * extracted from the Badge class. + */ +@Preview(name = "Western Arabic", locale = "en") +@Preview(name = "Eastern Arabic", locale = "ar") +@Preview(name = "Bengali", locale = "bn") +@Preview(name = "Burmese", locale = "mr") +@Preview(name = "Nepali", locale = "ne") +annotation class PreviewNumericSystems diff --git a/mobile/android/android-components/components/compose/base/src/test/java/mozilla/components/compose/base/badge/BadgedIconTest.kt b/mobile/android/android-components/components/compose/base/src/test/java/mozilla/components/compose/base/badge/BadgedIconTest.kt @@ -0,0 +1,326 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.compose.base.badge + +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onChild +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.ui.icons.R +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class BadgedIconTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun `WHEN badge size is small THEN icon size is 24x24`() { + val expectedSize = 24 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_SMALL, + notificationCount = 0, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val layoutInfo = badgedIcon.fetchSemanticsNode().layoutInfo + assertEquals(expectedSize, layoutInfo.height) + assertEquals(expectedSize, layoutInfo.width) + } + + @Test + fun `WHEN badge size is small THEN badge size is 8x8`() { + val expectedSize = 8 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_SMALL, + notificationCount = 0, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val badge = badgedIcon.onChild() + val layoutInfo = badge.fetchSemanticsNode().layoutInfo + assertEquals(expectedSize, layoutInfo.width) + assertEquals(expectedSize, layoutInfo.height) + } + + @Test + fun `WHEN badge size is large AND count is one digit THEN icon is 24x24`() { + val expectedWidth = 24 + val expectedHeight = 24 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 1, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val layoutInfo = badgedIcon.fetchSemanticsNode().layoutInfo + assertEquals(expectedWidth, layoutInfo.width) + assertEquals(expectedHeight, layoutInfo.height) + } + + @Test + fun `WHEN badge size is large AND count is one digit THEN badge size is 16x16`() { + val expectedWidth = 16 + val expectedHeight = 16 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 1, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val badge = badgedIcon.onChild() + val layoutInfo = badge.fetchSemanticsNode().layoutInfo + assertEquals(expectedWidth, layoutInfo.width) + assertEquals(expectedHeight, layoutInfo.height) + } + + @Test + fun `WHEN badge size is large AND count is two digits THEN icon size is 24x24`() { + val expectedWidth = 24 + val expectedHeight = 24 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 10, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val layoutInfo = badgedIcon.fetchSemanticsNode().layoutInfo + assertEquals(expectedWidth, layoutInfo.width) + assertEquals(expectedHeight, layoutInfo.height) + } + + @Test + fun `WHEN badge size is large AND count is two digits THEN badge size is 16x16dp`() { + val expectedWidth = 16 + val expectedHeight = 16 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 10, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val badge = badgedIcon.onChild() + val layoutInfo = badge.fetchSemanticsNode().layoutInfo + assertEquals(expectedWidth, layoutInfo.width) + assertEquals(expectedHeight, layoutInfo.height) + } + + @Test + fun `WHEN badge size is large AND count is three digits THEN icon size is 24x24`() { + val expectedWidth = 24 + val expectedHeight = 24 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 999, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val layoutInfo = badgedIcon.fetchSemanticsNode().layoutInfo + assertEquals(expectedWidth, layoutInfo.width) + assertEquals(expectedHeight, layoutInfo.height) + } + + @Test + fun `WHEN badge size is large AND count is max int THEN badge size is 16x16dp`() { + val expectedWidth = 16 + val expectedHeight = 16 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = Int.MAX_VALUE, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val badge = badgedIcon.onChild() + val layoutInfo = badge.fetchSemanticsNode().layoutInfo + assertEquals(expectedWidth, layoutInfo.width) + assertEquals(expectedHeight, layoutInfo.height) + } + + @Test + fun `WHEN badge size is large AND count is three digits THEN badge size is 16x16dp`() { + val expectedWidth = 16 + val expectedHeight = 16 + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 999, + ) + } + val badgedIcon = composeTestRule.onNodeWithTag("badgedIcon") + val badge = badgedIcon.onChild() + val layoutInfo = badge.fetchSemanticsNode().layoutInfo + assertEquals(expectedWidth, layoutInfo.width) + assertEquals(expectedHeight, layoutInfo.height) + } + + @Test + fun `WHEN badge size is small THEN count is NOT displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_SMALL, + notificationCount = 10, + ) + } + val textField = composeTestRule.onNodeWithText("10") + assertFalse(textField.isDisplayed()) + } + + @Test + fun `WHEN badge size is large THEN count is displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 10, + ) + } + val textField = composeTestRule.onNodeWithText("10") + assertTrue(textField.isDisplayed()) + } + + @Test + fun `WHEN badge size is large AND count is one digit THEN entire count is displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 1, + ) + } + val textField = composeTestRule.onNodeWithText("1") + assertTrue(textField.isDisplayed()) + } + + @Test + fun `WHEN badge size is large AND count is two digits THEN entire count is displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 20, + ) + } + val textField = composeTestRule.onNodeWithText("20") + assertTrue(textField.isDisplayed()) + } + + @Test + fun `WHEN badge size is large AND count is three digits THEN infinity symbol is displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = 100, + ) + } + val textField = composeTestRule.onNodeWithText("∞") + assertTrue(textField.isDisplayed()) + } + + @Test + fun `WHEN badge size is large AND count is int max THEN infinity symbol is displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = true, + size = BADGE_SIZE_LARGE, + notificationCount = Int.MAX_VALUE, + ) + } + val textField = composeTestRule.onNodeWithText("∞") + assertTrue(textField.isDisplayed()) + } + + @Test + fun `WHEN badge size is large AND not highlighted THEN only the icon is displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = false, + size = BADGE_SIZE_LARGE, + notificationCount = 100, + ) + } + val icon = composeTestRule.onNodeWithTag("badgedIcon") + assertTrue(icon.isDisplayed()) + val badge = icon.onChild() + assertFalse(badge.isDisplayed()) + } + + @Test + fun `WHEN badge size is small AND not highlighted THEN only the icon is displayed`() { + composeTestRule.setContent { + BadgedIcon( + modifier = Modifier.Companion.testTag("badgedIcon"), + painter = painterResource(R.drawable.mozac_ic_download_24), + isHighlighted = false, + size = BADGE_SIZE_SMALL, + notificationCount = 100, + ) + } + val icon = composeTestRule.onNodeWithTag("badgedIcon") + assertTrue(icon.isDisplayed()) + val badge = icon.onChild() + assertFalse(badge.isDisplayed()) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/LibraryMenuItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/LibraryMenuItem.kt @@ -19,9 +19,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -43,6 +40,8 @@ import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import mozilla.components.compose.base.badge.BADGE_SIZE_SMALL +import mozilla.components.compose.base.badge.BadgedIcon import mozilla.components.compose.base.theme.information import mozilla.components.compose.base.theme.surfaceDimVariant import org.mozilla.fenix.R @@ -98,19 +97,14 @@ fun LibraryMenuItem( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = 4.dp, vertical = 12.dp), ) { - BadgedBox( - badge = { - if (isHighlighted) { - Badge(containerColor = MaterialTheme.colorScheme.information) - } - }, - ) { - Icon( - painter = painterResource(iconRes), - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - } + BadgedIcon( + painter = painterResource(iconRes), + isHighlighted = isHighlighted, + size = BADGE_SIZE_SMALL, + contentDescription = null, + containerColor = MaterialTheme.colorScheme.information, + tint = MaterialTheme.colorScheme.onSurface, + ) Spacer(Modifier.height(4.dp)) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt @@ -24,8 +24,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -65,7 +63,10 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import mozilla.components.compose.base.badge.BADGE_SIZE_SMALL +import mozilla.components.compose.base.badge.BadgedIcon import mozilla.components.compose.base.modifier.thenConditional +import mozilla.components.compose.base.theme.information import mozilla.components.compose.base.theme.surfaceDimVariant import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.compose.button.RadioButton @@ -363,20 +364,15 @@ private fun IconListItemBeforeIcon( description: String?, tint: Color, ) { - BadgedBox( - badge = { - if (isHighlighted) { - Badge(containerColor = FirefoxTheme.colors.actionInformation) - } - }, - ) { - Icon( - painter = painter, - contentDescription = description, - tint = tint, - modifier = Modifier.size(ICON_SIZE), - ) - } + BadgedIcon( + painter = painter, + isHighlighted = isHighlighted, + tint = tint, + size = BADGE_SIZE_SMALL, + contentDescription = description, + containerColor = MaterialTheme.colorScheme.information, + modifier = Modifier.size(ICON_SIZE), + ) } @Composable @@ -1067,8 +1063,9 @@ private fun TextListItemWithIconPreview() { } } +@Suppress("LongMethod") @Composable -@Preview(name = "IconListItem", uiMode = Configuration.UI_MODE_NIGHT_YES) +@PreviewLightDark private fun IconListItemPreview() { FirefoxTheme { Column(Modifier.background(MaterialTheme.colorScheme.surface)) { @@ -1080,6 +1077,14 @@ private fun IconListItemPreview() { ) IconListItem( + label = "Left icon list item highlighted", + onClick = {}, + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + isBeforeIconHighlighted = true, + ) + + IconListItem( label = "Left icon list item", colors = ListItemDefaults.colors(headlineColor = MaterialTheme.colorScheme.tertiary), onClick = {}, @@ -1088,6 +1093,15 @@ private fun IconListItemPreview() { ) IconListItem( + label = "Left icon list item highlighted", + colors = ListItemDefaults.colors(headlineColor = MaterialTheme.colorScheme.tertiary), + onClick = {}, + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + isBeforeIconHighlighted = true, + ) + + IconListItem( label = "Left icon list item + right icon", onClick = {}, beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), @@ -1098,6 +1112,17 @@ private fun IconListItemPreview() { ) IconListItem( + label = "Left icon list item highlighted + right icon", + onClick = {}, + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + showDivider = true, + afterIconPainter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), + afterIconDescription = null, + isBeforeIconHighlighted = true, + ) + + IconListItem( label = "Left icon list item + right icon (disabled)", enabled = false, onClick = {}, @@ -1108,6 +1133,17 @@ private fun IconListItemPreview() { ) IconListItem( + label = "Left icon list item highlighted + right icon (disabled)", + enabled = false, + onClick = {}, + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + afterIconPainter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), + afterIconDescription = null, + isBeforeIconHighlighted = true, + ) + + IconListItem( label = "Left icon list item + right icon (disabled)", overline = "Overline", enabled = false, @@ -1128,6 +1164,18 @@ private fun IconListItemPreview() { afterIconPainter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), afterIconDescription = null, ) + + IconListItem( + label = "Left icon list item highlighted + right icon (disabled)", + overline = "Overline", + enabled = false, + onClick = {}, + beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + beforeIconDescription = "click me", + afterIconPainter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), + afterIconDescription = null, + isBeforeIconHighlighted = true, + ) } } }