tor-browser

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

commit c0c36dd8e023ff3c6b1eb2e55ae24b749a680bfd
parent 9ac0c56f0e8c4e169be9d68d0ce2d1352aab7f9f
Author: Cathy Lu <calu@mozilla.com>
Date:   Fri, 17 Oct 2025 17:06:47 +0000

Bug 1985834 - Update ListItem to M3 specs, add Switch and overline r=android-reviewers,007

Part 1 - Update ListItem spacing, colors, and shapes
Part 2 - Add overline, Switch, and enablement to ListItem
Part 3 - Remove dividers from ListItem

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

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt | 9++++-----
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt | 684+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
2 files changed, 410 insertions(+), 283 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/compose/MenuItem.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.unit.dp import org.mozilla.fenix.R import org.mozilla.fenix.components.menu.MenuDialogTestTag.WEB_EXTENSION_ITEM import org.mozilla.fenix.compose.list.IconListItem -import org.mozilla.fenix.compose.list.ImageListItem import org.mozilla.fenix.compose.list.TextListItem import org.mozilla.fenix.theme.FirefoxTheme import mozilla.components.ui.icons.R as iconsR @@ -226,11 +225,11 @@ internal fun WebExtensionMenuItem( onClick: (() -> Unit)? = null, onSettingsClick: (() -> Unit)? = null, ) { - ImageListItem( + IconListItem( label = label, - iconPainter = iconPainter, enabled = enabled == true, - iconTint = iconTint, + beforeIconTint = iconTint ?: FirefoxTheme.colors.iconPrimary, + beforeIconPainter = iconPainter, onClick = onClick, modifier = Modifier .testTag(WEB_EXTENSION_ITEM) @@ -251,7 +250,7 @@ internal fun WebExtensionMenuItem( .background( color = FirefoxTheme.colors.layer3, ), - afterListItemAction = { + afterListAction = { Row( modifier = Modifier.padding(start = 16.dp), verticalAlignment = Alignment.CenterVertically, 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 @@ -10,34 +10,36 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape 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 +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateOf 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.ColorFilter import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -50,17 +52,20 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.PlatformTextStyle +import androidx.compose.ui.text.style.Hyphens import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextOverflow 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.Dp import androidx.compose.ui.unit.dp import mozilla.components.compose.base.modifier.thenConditional -import mozilla.components.ui.colors.PhotonColors import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.compose.button.RadioButton import org.mozilla.fenix.theme.FirefoxTheme +import java.util.Locale import mozilla.components.ui.icons.R as iconsR private val LIST_ITEM_HEIGHT = 56.dp @@ -68,46 +73,7 @@ private val ICON_SIZE = 24.dp private const val TOAST_LENGTH = Toast.LENGTH_SHORT -/** - * List item used to display a image with optional Composable for adding UI to the end of the list item. - * - * @param label The label in the list item. - * @param iconPainter [Painter] used to display an [Icon] at the beginning of the list item. - * @param enabled Controls the enabled state of the list item. When `false`, the list item will not - * be clickable. - * @param modifier [Modifier] to be applied to the layout. - * @param iconTint Tint color to be applied on the [Icon]. - * @param onClick Called when the user clicks on the item. - * @param afterListItemAction Optional Composable for adding UI to the end of the list item. - */ -@Composable -fun ImageListItem( - label: String, - iconPainter: Painter, - enabled: Boolean, - modifier: Modifier = Modifier, - iconTint: Color? = null, - onClick: (() -> Unit)? = null, - afterListItemAction: @Composable RowScope.() -> Unit = {}, -) { - ListItem( - label = label, - modifier = modifier, - enabled = enabled, - onClick = onClick, - beforeListItemAction = { - Image( - painter = iconPainter, - contentDescription = null, - modifier = Modifier.size(ICON_SIZE), - colorFilter = iconTint?.let { ColorFilter.tint(it) }, - ) - - Spacer(modifier = Modifier.width(16.dp)) - }, - afterListItemAction = afterListItemAction, - ) -} +private val EmptyListItemSlot: @Composable RowScope.() -> Unit = {} /** * List item used to display a label with an optional description text and an optional @@ -116,6 +82,7 @@ fun ImageListItem( * @param label The label in the list item. * @param modifier [Modifier] to be applied to the layout. * @param maxLabelLines An optional maximum number of lines for the label text to span. + * @param overline An optional text shown above the label. * @param description An optional description text below the label. * @param maxDescriptionLines An optional maximum number of lines for the description text to span. * @param minHeight An optional minimum height for the list item. @@ -132,6 +99,7 @@ fun TextListItem( label: String, modifier: Modifier = Modifier, maxLabelLines: Int = 1, + overline: String? = null, description: String? = null, maxDescriptionLines: Int = 1, minHeight: Dp = LIST_ITEM_HEIGHT, @@ -139,13 +107,14 @@ fun TextListItem( onLongClick: (() -> Unit)? = null, iconPainter: Painter? = null, iconDescription: String? = null, - iconTint: Color = FirefoxTheme.colors.iconPrimary, + iconTint: Color = ListItemDefaults.colors().leadingIconColor, onIconClick: (() -> Unit)? = null, ) { ListItem( label = label, maxLabelLines = maxLabelLines, modifier = modifier, + overline = overline, description = description, maxDescriptionLines = maxDescriptionLines, minHeight = minHeight, @@ -156,8 +125,6 @@ fun TextListItem( return@ListItem } - Spacer(modifier = Modifier.width(16.dp)) - if (onIconClick == null) { Icon( painter = iconPainter, @@ -190,6 +157,7 @@ fun TextListItem( * @param modifier [Modifier] to be applied to the layout. * @param faviconShape The shape used to clip the favicon. Defaults to a slightly rounded rectangle. * @param labelModifier [Modifier] to be applied to the label. + * @param overline An optional text shown above the label. * @param description An optional description text below the label. * @param maxDescriptionLines An optional maximum number of lines for the description text to span. * @param faviconPainter Optional painter to use when fetching a new favicon is unnecessary. @@ -209,6 +177,7 @@ fun FaviconListItem( modifier: Modifier = Modifier, faviconShape: Shape = RoundedCornerShape(2.dp), labelModifier: Modifier = Modifier, + overline: String? = null, description: String? = null, maxDescriptionLines: Int = 1, faviconPainter: Painter? = null, @@ -224,6 +193,7 @@ fun FaviconListItem( label = label, modifier = modifier, labelModifier = labelModifier, + overline = overline, description = description, maxDescriptionLines = maxDescriptionLines, onClick = onClick, @@ -242,8 +212,6 @@ fun FaviconListItem( shape = faviconShape, ) } - - Spacer(modifier = Modifier.width(16.dp)) }, afterListItemAction = { if (iconPainter == null || onIconClick == null) { @@ -251,13 +219,9 @@ fun FaviconListItem( } if (showDivider) { - Spacer(modifier = Modifier.width(8.dp)) - VerticalDivider() } - Spacer(modifier = Modifier.width(16.dp)) - IconButton( onClick = onIconClick, modifier = iconButtonModifier.then( @@ -268,7 +232,6 @@ fun FaviconListItem( Icon( painter = iconPainter, contentDescription = iconDescription, - tint = FirefoxTheme.colors.iconPrimary, ) } }, @@ -283,6 +246,8 @@ fun FaviconListItem( * @param modifier [Modifier] to be applied to the layout. * @param labelModifier [Modifier] to be applied to the label. * @param labelTextColor [Color] to be applied to the label. + * @param overline An optional text shown above the label. + * @param overlineTextColor [Color] to be applied to the overline text. * @param descriptionTextColor [Color] to be applied to the description. * @param maxLabelLines An optional maximum number of lines for the label text to span. * @param description An optional description text below the label. @@ -310,8 +275,10 @@ fun IconListItem( label: String, modifier: Modifier = Modifier, labelModifier: Modifier = Modifier, - labelTextColor: Color = FirefoxTheme.colors.textPrimary, - descriptionTextColor: Color = FirefoxTheme.colors.textSecondary, + labelTextColor: Color = ListItemDefaults.colors().headlineColor, + overline: String? = null, + overlineTextColor: Color = ListItemDefaults.colors().overlineColor, + descriptionTextColor: Color = ListItemDefaults.colors().supportingTextColor, maxLabelLines: Int = 1, description: String? = null, maxDescriptionLines: Int = 1, @@ -321,12 +288,12 @@ fun IconListItem( onLongClick: (() -> Unit)? = null, beforeIconPainter: Painter, beforeIconDescription: String? = null, - beforeIconTint: Color = FirefoxTheme.colors.iconPrimary, + beforeIconTint: Color = ListItemDefaults.colors().leadingIconColor, isBeforeIconHighlighted: Boolean = false, showDivider: Boolean = false, afterIconPainter: Painter? = null, afterIconDescription: String? = null, - afterIconTint: Color = FirefoxTheme.colors.iconPrimary, + afterIconTint: Color = ListItemDefaults.colors().trailingIconColor, onAfterIconClick: (() -> Unit)? = null, afterListAction: (@Composable () -> Unit)? = null, ) { @@ -335,6 +302,8 @@ fun IconListItem( modifier = modifier, labelModifier = labelModifier, labelTextColor = labelTextColor, + overline = overline, + overlineTextColor = overlineTextColor, descriptionTextColor = descriptionTextColor, maxLabelLines = maxLabelLines, description = description, @@ -348,18 +317,18 @@ fun IconListItem( isHighlighted = isBeforeIconHighlighted, painter = beforeIconPainter, description = beforeIconDescription, - tint = if (enabled) beforeIconTint else FirefoxTheme.colors.iconDisabled, + tint = if (enabled) beforeIconTint else ListItemDefaults.colors().disabledLeadingIconColor, ) }, afterListItemAction = { IconListItemAfterIcon( - showDivider = showDivider, enabled = enabled, painter = afterIconPainter, description = afterIconDescription, - tint = if (enabled) afterIconTint else FirefoxTheme.colors.iconDisabled, + tint = if (enabled) afterIconTint else ListItemDefaults.colors().disabledTrailingIconColor, onClick = onAfterIconClick, listAction = afterListAction, + showDivider = showDivider, ) }, ) @@ -385,8 +354,6 @@ private fun IconListItemBeforeIcon( tint = tint, ) } - - Spacer(modifier = Modifier.width(16.dp)) } @Composable @@ -409,13 +376,9 @@ private fun IconListItemAfterIcon( } if (showDivider) { - Spacer(modifier = Modifier.width(8.dp)) - VerticalDivider() } - Spacer(modifier = Modifier.width(16.dp)) - if (onClick == null) { Icon( painter = painter, @@ -443,14 +406,18 @@ private fun IconListItemAfterIcon( /** * List item used to display a label with an optional description text and - * a [RadioButton] at the beginning. + * a [RadioButton] at the beginning or at the end. * * @param label The label in the list item. * @param selected [Boolean] That indicates whether the [RadioButton] is currently selected. * @param modifier [Modifier] to be applied to the layout. + * @param overline An optional text shown above the label. * @param maxLabelLines An optional maximum number of lines for the label text to span. * @param description An optional description text below the label. * @param maxDescriptionLines An optional maximum number of lines for the description text to span. + * @param enabled Controls the enabled state of the list item. When `false`, the list item will not + * be clickable. + * @param showButtonAfter [Boolean] That indicates whether the [RadioButton] is after the [ListItem]. * @param onClick Called when the user clicks on the item. */ @Composable @@ -458,11 +425,24 @@ fun RadioButtonListItem( label: String, selected: Boolean, modifier: Modifier = Modifier, + overline: String? = null, maxLabelLines: Int = 1, description: String? = null, maxDescriptionLines: Int = 1, + enabled: Boolean = true, + showButtonAfter: Boolean = false, onClick: (() -> Unit), ) { + val radioButton: @Composable RowScope.() -> Unit = { + RadioButton( + selected = selected, + modifier = Modifier + .size(ICON_SIZE) + .clearAndSetSemantics {}, + enabled = enabled, + onClick = onClick, + ) + } ListItem( label = label, modifier = modifier @@ -471,31 +451,81 @@ fun RadioButtonListItem( role = Role.RadioButton }, maxLabelLines = maxLabelLines, + overline = overline, description = description, maxDescriptionLines = maxDescriptionLines, + enabled = enabled, onClick = onClick, - beforeListItemAction = { - RadioButton( - selected = selected, - modifier = Modifier - .size(ICON_SIZE) - .clearAndSetSemantics {}, - onClick = onClick, - ) + beforeListItemAction = if (showButtonAfter) radioButton else EmptyListItemSlot, + afterListItemAction = if (showButtonAfter) radioButton else EmptyListItemSlot, + ) +} + +/** + * List item used to display a label with an optional description text and + * a [Switch] at the beginning or at the end. + * + * @param label The label in the list item. + * @param checked [Boolean] That indicates whether the [Switch] is currently checked. + * @param modifier [Modifier] to be applied to the layout. + * @param overline An optional text shown above the label. + * @param maxLabelLines An optional maximum number of lines for the label text to span. + * @param description An optional description text below the label. + * @param maxDescriptionLines An optional maximum number of lines for the description text to span. + * @param enabled Controls the enabled state of the list item. When `false`, the list item will not + * be clickable. + * @param showSwitchAfter [Boolean] That indicates whether the [RadioButton] is after the [ListItem]. + * @param onClick Called when the user clicks the [Switch]. + */ +@Composable +fun SwitchListItem( + label: String, + checked: Boolean, + modifier: Modifier = Modifier, + overline: String? = null, + maxLabelLines: Int = 1, + description: String? = null, + maxDescriptionLines: Int = 1, + enabled: Boolean = true, + showSwitchAfter: Boolean = false, + onClick: (Boolean) -> Unit, +) { + val switch: @Composable RowScope.() -> Unit = { + Switch( + checked = checked, + onCheckedChange = onClick, + enabled = enabled, + modifier = Modifier + .clearAndSetSemantics {}, + ) + } - Spacer(modifier = Modifier.width(32.dp)) + ListItem( + label = label, + modifier = modifier.semantics(mergeDescendants = true) { + this.selected = checked + role = Role.Switch }, + maxLabelLines = maxLabelLines, + overline = overline, + description = description, + maxDescriptionLines = maxDescriptionLines, + enabled = enabled, + onClick = { onClick(!checked) }, + beforeListItemAction = if (showSwitchAfter) switch else EmptyListItemSlot, + afterListItemAction = if (showSwitchAfter) switch else EmptyListItemSlot, ) } /** - * Selectable list item used to display a label and a [Favicon] with an optional description text and - * an optional [IconButton] at the end. + * Selectable list item used to display a label and a [Favicon] with an optional description text + * at either the beginning or the end and an optional [IconButton] at the end. * * @param label The label in the list item. * @param url Website [url] for which the favicon will be shown. * @param isSelected The selected state of the item. * @param modifier [Modifier] to be applied to the layout. + * @param overline An optional text shown above the label. * @param description An optional description text below the label. * @param faviconPainter Optional painter to use when fetching a new favicon is unnecessary. * @param onClick Called when the user clicks on the item. @@ -513,6 +543,7 @@ fun SelectableFaviconListItem( url: String, isSelected: Boolean, modifier: Modifier = Modifier, + overline: String? = null, description: String? = null, faviconPainter: Painter? = null, onClick: (() -> Unit)? = null, @@ -526,6 +557,7 @@ fun SelectableFaviconListItem( ListItem( label = label, modifier = modifier, + overline = overline, description = description, onClick = onClick, onLongClick = onLongClick, @@ -540,15 +572,10 @@ fun SelectableFaviconListItem( modifier = Modifier.size(ICON_SIZE), ) } else { - Favicon( - url = url, - size = ICON_SIZE, - ) + Favicon(url = url, size = ICON_SIZE) } }, ) - - Spacer(modifier = Modifier.width(16.dp)) }, afterListItemAction = { if ((iconPainter == null || onIconClick == null) && iconSlot == null) { @@ -556,13 +583,9 @@ fun SelectableFaviconListItem( } if (showDivider) { - Spacer(modifier = Modifier.width(8.dp)) - VerticalDivider() } - Spacer(modifier = Modifier.width(16.dp)) - when { iconPainter != null && onIconClick != null -> { IconButton( @@ -572,7 +595,7 @@ fun SelectableFaviconListItem( Icon( painter = iconPainter, contentDescription = iconDescription, - tint = FirefoxTheme.colors.iconPrimary, + tint = ListItemDefaults.colors().trailingIconColor, ) } } @@ -591,6 +614,8 @@ fun SelectableFaviconListItem( * @param modifier [Modifier] to be applied to the layout. * @param labelModifier [Modifier] to be applied to the label layout. * @param labelTextColor [Color] to be applied to the label. + * @param overline An optional text shown above the label. + * @param overlineTextColor [Color] to be applied to the overline text. * @param descriptionTextColor [Color] to be applied to the description. * @param maxLabelLines An optional maximum number of lines for the label text to span. * @param description An optional description text below the label. @@ -618,8 +643,10 @@ fun SelectableIconListItem( isSelected: Boolean, modifier: Modifier = Modifier, labelModifier: Modifier = modifier, - labelTextColor: Color = FirefoxTheme.colors.textPrimary, - descriptionTextColor: Color = FirefoxTheme.colors.textSecondary, + labelTextColor: Color = ListItemDefaults.colors().headlineColor, + overline: String? = null, + overlineTextColor: Color = ListItemDefaults.colors().overlineColor, + descriptionTextColor: Color = ListItemDefaults.colors().supportingTextColor, maxLabelLines: Int = 1, description: String? = null, enabled: Boolean = true, @@ -628,11 +655,11 @@ fun SelectableIconListItem( onLongClick: (() -> Unit)? = null, beforeIconPainter: Painter, beforeIconDescription: String? = null, - beforeIconTint: Color = FirefoxTheme.colors.iconPrimary, + beforeIconTint: Color = ListItemDefaults.colors().leadingIconColor, showDivider: Boolean = false, afterIconPainter: Painter? = null, afterIconDescription: String? = null, - afterIconTint: Color = FirefoxTheme.colors.iconPrimary, + afterIconTint: Color = ListItemDefaults.colors().trailingIconColor, onAfterIconClick: (() -> Unit)? = null, iconSlot: (@Composable () -> Unit)? = null, ) { @@ -641,6 +668,8 @@ fun SelectableIconListItem( modifier = modifier, labelModifier = labelModifier, labelTextColor = labelTextColor, + overline = overline, + overlineTextColor = overlineTextColor, descriptionTextColor = descriptionTextColor, maxLabelLines = maxLabelLines, description = description, @@ -655,28 +684,22 @@ fun SelectableIconListItem( Icon( painter = beforeIconPainter, contentDescription = beforeIconDescription, - tint = if (enabled) beforeIconTint else FirefoxTheme.colors.iconDisabled, + tint = if (enabled) beforeIconTint else ListItemDefaults.colors().disabledLeadingIconColor, ) }, ) - - Spacer(modifier = Modifier.width(16.dp)) }, afterListItemAction = { if (afterIconPainter == null && iconSlot == null) { return@ListItem } - val tint = if (enabled) afterIconTint else FirefoxTheme.colors.iconDisabled + val tint = if (enabled) afterIconTint else ListItemDefaults.colors().disabledTrailingIconColor if (showDivider) { - Spacer(modifier = Modifier.width(8.dp)) - VerticalDivider() } - Spacer(modifier = Modifier.width(16.dp)) - when { afterIconPainter != null -> { if (onAfterIconClick == null) { @@ -714,11 +737,13 @@ fun SelectableIconListItem( * @param icon The icon resource to be displayed at the beginning of the list item. * @param isSelected The selected state of the item. * @param modifier [Modifier] to be applied to the composable. + * @param overline An optional text shown above the label. * @param descriptionTextColor [Color] to be applied to the description. * @param iconTint Tint to be applied to [icon]. * @param labelOverflow How visual overflow should be handled for the label. * @param afterListItemAction Composable for adding UI to the end of the list item. * @param belowListItemContent Composable for adding UI to the bottom of the list item content. + * @param showSelectableItemAfter [Boolean] That indicates whether the [Icon] is after the [ListItem]. */ @Composable fun SelectableListItem( @@ -727,34 +752,45 @@ fun SelectableListItem( @DrawableRes icon: Int, isSelected: Boolean, modifier: Modifier = Modifier, - descriptionTextColor: Color = FirefoxTheme.colors.textSecondary, - iconTint: Color = FirefoxTheme.colors.iconPrimary, + overline: String? = null, + descriptionTextColor: Color = ListItemDefaults.colors().supportingTextColor, + iconTint: Color = ListItemDefaults.colors().leadingIconColor, labelOverflow: TextOverflow = TextOverflow.Ellipsis, afterListItemAction: @Composable RowScope.() -> Unit, belowListItemContent: @Composable ColumnScope.() -> Unit = {}, + showSelectableItemAfter: Boolean = false, ) { + val selectableItem: @Composable RowScope.() -> Unit = { + SelectableItemIcon( + icon = { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + tint = iconTint, + ) + }, + isSelected = isSelected, + ) + } ListItem( label = label, description = description, modifier = modifier, + overline = overline, descriptionTextColor = descriptionTextColor, belowListItemContent = belowListItemContent, labelOverflow = labelOverflow, beforeListItemAction = { - SelectableItemIcon( - icon = { - Icon( - painter = painterResource(id = icon), - contentDescription = null, - tint = iconTint, - ) - }, - isSelected = isSelected, - ) - - Spacer(modifier = Modifier.width(16.dp)) + if (!showSelectableItemAfter) { + selectableItem() + } }, - afterListItemAction = afterListItemAction, + afterListItemAction = + if (showSelectableItemAfter) { + selectableItem + } else { + afterListItemAction + }, ) } @@ -770,22 +806,11 @@ private fun SelectableItemIcon( icon: @Composable () -> Unit, ) { if (isSelected) { - Box( - modifier = Modifier - .background( - color = FirefoxTheme.colors.layerAccent, - shape = CircleShape, - ) - .size(24.dp), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = iconsR.drawable.mozac_ic_checkmark_24), - contentDescription = null, - modifier = Modifier.size(12.dp), - tint = PhotonColors.White, - ) - } + Checkbox( + checked = true, + onCheckedChange = null, + modifier = Modifier.size(18.dp), + ) } else { icon() } @@ -797,6 +822,8 @@ private fun SelectableItemIcon( * * @param label The label in the list item. * @param modifier [Modifier] to be applied to the layout. + * @param overline An optional text shown above the label. + * @param overlineTextColor [Color] to be applied to the overline text. * @param labelModifier [Modifier] to be applied to the label. * @param labelTextColor [Color] to be applied to the label. * @param descriptionTextColor [Color] to be applied to the description. @@ -817,9 +844,11 @@ private fun SelectableItemIcon( private fun ListItem( label: String, modifier: Modifier = Modifier, + overline: String? = null, + overlineTextColor: Color = ListItemDefaults.colors().overlineColor, labelModifier: Modifier = Modifier, - labelTextColor: Color = FirefoxTheme.colors.textPrimary, - descriptionTextColor: Color = FirefoxTheme.colors.textSecondary, + labelTextColor: Color = ListItemDefaults.colors().headlineColor, + descriptionTextColor: Color = ListItemDefaults.colors().supportingTextColor, labelOverflow: TextOverflow = TextOverflow.Ellipsis, maxLabelLines: Int = 1, description: String? = null, @@ -833,45 +862,56 @@ private fun ListItem( afterListItemAction: @Composable RowScope.() -> Unit = {}, ) { val haptics = LocalHapticFeedback.current - Row( - modifier = modifier - .height(IntrinsicSize.Min) - .defaultMinSize(minHeight = minHeight) - .thenConditional( - modifier = Modifier.combinedClickable( - onClick = { onClick?.invoke() }, - onLongClick = { - onLongClick?.let { - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - it.invoke() - } - }, + val contentColor = if (enabled) { + ListItemDefaults.contentColor + } else { + ListItemDefaults.colors().disabledLeadingIconColor + } + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Row( + modifier = modifier + .height(IntrinsicSize.Min) + .defaultMinSize(minHeight = minHeight) + .thenConditional( + modifier = Modifier.combinedClickable( + onClick = { onClick?.invoke() }, + onLongClick = { + onLongClick?.let { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + it.invoke() + } + }, + ), + predicate = { (onClick != null || onLongClick != null) && enabled }, + ) + .padding( + horizontal = FirefoxTheme.layout.space.dynamic200, + vertical = FirefoxTheme.layout.space.static150, ), - predicate = { (onClick != null || onLongClick != null) && enabled }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(FirefoxTheme.layout.space.static200), + ) { + beforeListItemAction() + + ListItemContent( + label = label, + modifier = Modifier.weight(1f), + labelModifier = labelModifier, + labelTextColor = labelTextColor, + overline = overline, + overlineTextColor = overlineTextColor, + descriptionTextColor = descriptionTextColor, + labelOverflow = labelOverflow, + maxLabelLines = maxLabelLines, + description = description, + maxDescriptionLines = maxDescriptionLines, + enabled = enabled, + belowListItemContent = belowListItemContent, ) - .padding( - horizontal = FirefoxTheme.layout.space.dynamic200, - vertical = FirefoxTheme.layout.space.static100, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - beforeListItemAction() - - ListItemContent( - label = label, - modifier = Modifier.weight(1f), - labelModifier = labelModifier, - labelTextColor = labelTextColor, - descriptionTextColor = descriptionTextColor, - labelOverflow = labelOverflow, - maxLabelLines = maxLabelLines, - description = description, - maxDescriptionLines = maxDescriptionLines, - enabled = enabled, - belowListItemContent = belowListItemContent, - ) - afterListItemAction() + afterListItemAction() + } } } @@ -880,8 +920,10 @@ private fun ListItemContent( label: String, modifier: Modifier = Modifier, labelModifier: Modifier = Modifier, - labelTextColor: Color = FirefoxTheme.colors.textPrimary, - descriptionTextColor: Color = FirefoxTheme.colors.textSecondary, + labelTextColor: Color = ListItemDefaults.colors().headlineColor, + overline: String? = null, + overlineTextColor: Color = ListItemDefaults.colors().overlineColor, + descriptionTextColor: Color = ListItemDefaults.colors().supportingTextColor, labelOverflow: TextOverflow = TextOverflow.Ellipsis, maxLabelLines: Int = 1, description: String? = null, @@ -892,21 +934,28 @@ private fun ListItemContent( Column( modifier = modifier, ) { + overline?.let { + Text( + text = it.uppercase(Locale.getDefault()), + color = overlineTextColor, + style = FirefoxTheme.typography.overline.copy(hyphens = Hyphens.Auto), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } Text( text = label, modifier = labelModifier, - color = if (enabled) labelTextColor else FirefoxTheme.colors.textDisabled, + color = if (enabled) labelTextColor else ListItemDefaults.colors().disabledHeadlineColor, overflow = labelOverflow, - style = FirefoxTheme.typography.subtitle1.merge( - platformStyle = PlatformTextStyle(includeFontPadding = true), - ), + style = FirefoxTheme.typography.subtitle1.copy(hyphens = Hyphens.Auto), maxLines = maxLabelLines, ) description?.let { Text( text = description, - color = if (enabled) descriptionTextColor else FirefoxTheme.colors.textDisabled, + color = descriptionTextColor, overflow = TextOverflow.Ellipsis, maxLines = maxDescriptionLines, style = FirefoxTheme.typography.body2 @@ -950,6 +999,20 @@ private fun TextListItemWithDescriptionPreview() { } @Composable +@Preview(name = "TextListItem with overline and a description", uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TextListItemWithOverLineDescriptionPreview() { + FirefoxTheme { + Box(Modifier.background(MaterialTheme.colorScheme.surface)) { + TextListItem( + label = "Label + description", + overline = "Overline", + description = "Description text", + ) + } + } +} + +@Composable @Preview(name = "TextListItem with a right icon", uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TextListItemWithIconPreview() { FirefoxTheme { @@ -969,6 +1032,14 @@ private fun TextListItemWithIconPreview() { iconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), iconDescription = "click me", ) + + TextListItem( + label = "Label + right icon", + overline = "Overline", + onClick = {}, + iconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), + iconDescription = "click me", + ) } } } @@ -987,11 +1058,9 @@ private fun IconListItemPreview() { IconListItem( label = "Left icon list item", - labelTextColor = FirefoxTheme.colors.textAccent, onClick = {}, beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), beforeIconDescription = "click me", - beforeIconTint = FirefoxTheme.colors.iconAccentViolet, ) IconListItem( @@ -999,6 +1068,7 @@ private fun IconListItemPreview() { 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, ) @@ -1012,6 +1082,17 @@ private fun IconListItemPreview() { afterIconPainter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), afterIconDescription = null, ) + + IconListItem( + label = "Left icon list item + 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, + ) } } } @@ -1033,16 +1114,6 @@ private fun IconListItemWithAfterListActionPreview() { afterIconDescription = "click me", onAfterIconClick = { Toast.makeText(context, "icon click", TOAST_LENGTH).show() }, ) - - IconListItem( - label = "IconListItem + right icon + divider + clicks", - beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), - beforeIconDescription = null, - showDivider = true, - afterIconPainter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), - afterIconDescription = "click me", - onAfterIconClick = { Toast.makeText(context, "icon click", TOAST_LENGTH).show() }, - ) } } } @@ -1063,20 +1134,20 @@ private fun FaviconListItemPreview() { onClick = { Toast.makeText(context, "list item click", TOAST_LENGTH).show() }, iconPainter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), onIconClick = { Toast.makeText(context, "icon click", TOAST_LENGTH).show() }, + showDivider = true, ) FaviconListItem( - label = "Favicon + right icon + show divider + clicks", + label = "Favicon + painter", url = "", description = "Description text", + faviconPainter = painterResource(id = iconsR.drawable.mozac_ic_collection_24), onClick = { Toast.makeText(context, "list item click", TOAST_LENGTH).show() }, - showDivider = true, - iconPainter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), - onIconClick = { Toast.makeText(context, "icon click", TOAST_LENGTH).show() }, ) FaviconListItem( label = "Favicon + painter", + overline = "Overline", url = "", description = "Description text", faviconPainter = painterResource(id = iconsR.drawable.mozac_ic_collection_24), @@ -1088,29 +1159,6 @@ private fun FaviconListItemPreview() { @Composable @PreviewLightDark -private fun ImageListItemPreview() { - FirefoxTheme { - Column(Modifier.background(MaterialTheme.colorScheme.surface)) { - ImageListItem( - label = "label", - iconPainter = painterResource(iconsR.drawable.mozac_ic_web_extension_default_icon), - enabled = true, - onClick = {}, - afterListItemAction = { - Text( - text = "afterListItemText", - color = Color.White, - style = FirefoxTheme.typography.subtitle1, - maxLines = 1, - ) - }, - ) - } - } -} - -@Composable -@PreviewLightDark private fun RadioButtonListItemPreview() { val radioOptions = listOf("Radio button first item", "Radio button second item", "Radio button third item") @@ -1120,84 +1168,121 @@ private fun RadioButtonListItemPreview() { radioOptions.forEach { text -> RadioButtonListItem( label = text, + overline = "Overline", description = "$text description", onClick = { onOptionSelected(text) }, selected = (text == selectedOption), ) } + radioOptions.forEach { text -> + RadioButtonListItem( + label = text, + selected = (text == selectedOption), + overline = "Overline", + description = "$text description", + enabled = false, + showButtonAfter = true, + onClick = { onOptionSelected(text) }, + ) + } } } } @Composable @PreviewLightDark -private fun SelectableFaviconListItemPreview() { +private fun SwitchListItemPreview() { FirefoxTheme { Column(Modifier.background(MaterialTheme.colorScheme.surface)) { - SelectableFaviconListItem( - label = "Favicon + right icon", - url = "", - isSelected = false, - description = "Description text", + SwitchListItem( + label = "Switch item", + overline = "Overline", + description = "Switch item description", + checked = true, onClick = { }, - onLongClick = { }, - iconPainter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), - onIconClick = { }, ) - - SelectableFaviconListItem( - label = "Selected favicon + right icon", - url = "", - isSelected = true, - description = "Description text", + SwitchListItem( + label = "Switch item", + overline = "Overline", + description = "Switch item description", + checked = true, + enabled = false, + showSwitchAfter = true, onClick = { }, - onLongClick = { }, - iconPainter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), - onIconClick = { }, ) + } + } +} - SelectableFaviconListItem( - label = "Favicon + right icon + show divider", - url = "", - isSelected = false, - description = "Description text", - onClick = { }, - onLongClick = { }, - showDivider = true, - iconPainter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), - onIconClick = { }, - ) +private data class SelectableFaviconListItemPreviewState( + val label: String, + val url: String = "", + val isSelected: Boolean = false, + val overline: String? = null, + val description: String? = "Description text", + val faviconRes: Int? = null, + val onClick: (() -> Unit)? = { }, + val onLongClick: (() -> Unit)? = { }, + val showFaviconAfter: Boolean = false, + val iconRes: Int? = null, + val onIconClick: (() -> Unit)? = { }, +) - SelectableFaviconListItem( - label = "Selected favicon + right icon + show divider", - url = "", +private class SelectableFaviconListItemParameterProvider : + PreviewParameterProvider<SelectableFaviconListItemPreviewState> { + override val values: Sequence<SelectableFaviconListItemPreviewState> + get() = sequenceOf( + SelectableFaviconListItemPreviewState( + label = "Favicon + right icon", + faviconRes = iconsR.drawable.mozac_ic_collection_24, + iconRes = iconsR.drawable.mozac_ic_ellipsis_vertical_24, + ), + SelectableFaviconListItemPreviewState( + label = "Favicon + right icon + overline", + overline = "Overline", + faviconRes = iconsR.drawable.mozac_ic_collection_24, + iconRes = iconsR.drawable.mozac_ic_ellipsis_vertical_24, + ), + SelectableFaviconListItemPreviewState( + label = "Selected favicon + right icon", isSelected = true, - description = "Description text", - onClick = { }, - onLongClick = { }, - showDivider = true, - iconPainter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), - onIconClick = { }, - ) - - SelectableFaviconListItem( + faviconRes = iconsR.drawable.mozac_ic_collection_24, + iconRes = iconsR.drawable.mozac_ic_ellipsis_vertical_24, + ), + SelectableFaviconListItemPreviewState( label = "Favicon + painter", - url = "", - isSelected = false, - description = "Description text", - faviconPainter = painterResource(id = iconsR.drawable.mozac_ic_collection_24), - onClick = { }, - onLongClick = { }, - ) - - SelectableFaviconListItem( + faviconRes = iconsR.drawable.mozac_ic_collection_24, + ), + SelectableFaviconListItemPreviewState( label = "Selected favicon + painter", - url = "", + faviconRes = iconsR.drawable.mozac_ic_collection_24, isSelected = true, - description = "Description text", - faviconPainter = painterResource(id = iconsR.drawable.mozac_ic_collection_24), - onClick = { }, - onLongClick = { }, + ), + ) +} + +@Composable +@PreviewLightDark +private fun SelectableFaviconListItemPreview( + @PreviewParameter(SelectableFaviconListItemParameterProvider::class) state: SelectableFaviconListItemPreviewState, +) { + val faviconPainter = state.faviconRes?.let { painterResource(it) } + val iconPainter = state.iconRes?.let { painterResource(it) } + + FirefoxTheme { + Column(Modifier.background(MaterialTheme.colorScheme.surface)) { + SelectableFaviconListItem( + label = state.label, + url = state.url, + isSelected = state.isSelected, + overline = state.overline, + description = state.description, + faviconPainter = faviconPainter, + onClick = state.onClick, + onLongClick = state.onLongClick, + showDivider = false, + iconPainter = iconPainter, + onIconClick = {}, ) } } @@ -1228,21 +1313,17 @@ private fun SelectableIconListItemPreview() { SelectableIconListItem( label = "Left icon list item", isSelected = false, - labelTextColor = FirefoxTheme.colors.textAccent, onClick = {}, beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), beforeIconDescription = "click me", - beforeIconTint = FirefoxTheme.colors.iconAccentViolet, ) SelectableIconListItem( label = "Selected left icon list item", isSelected = true, - labelTextColor = FirefoxTheme.colors.textAccent, onClick = {}, beforeIconPainter = painterResource(iconsR.drawable.mozac_ic_folder_24), beforeIconDescription = "click me", - beforeIconTint = FirefoxTheme.colors.iconAccentViolet, ) SelectableIconListItem( @@ -1286,6 +1367,18 @@ private fun SelectableIconListItemPreview() { afterIconPainter = painterResource(iconsR.drawable.mozac_ic_chevron_right_24), afterIconDescription = null, ) + + SelectableIconListItem( + label = "Selected left icon list item + right icon (disabled)", + isSelected = true, + 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, + ) } } } @@ -1323,12 +1416,47 @@ private fun SelectableListItemPreview() { ) { Icon( painter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), - tint = FirefoxTheme.colors.iconPrimary, contentDescription = null, ) } }, ) + + SelectableListItem( + label = "Non selectable item", + description = "with after action", + icon = iconsR.drawable.mozac_ic_folder_24, + isSelected = false, + overline = "Overline", + afterListItemAction = { + IconButton( + onClick = {}, + modifier = Modifier.size(ICON_SIZE), + ) { + Icon( + painter = painterResource(iconsR.drawable.mozac_ic_ellipsis_vertical_24), + contentDescription = null, + ) + } + }, + ) + SelectableListItem( + label = "Selected item", + description = "Description text", + icon = iconsR.drawable.mozac_ic_folder_24, + isSelected = true, + afterListItemAction = {}, + showSelectableItemAfter = true, + ) + + SelectableListItem( + label = "Non selectable item", + description = "without after action", + icon = iconsR.drawable.mozac_ic_folder_24, + isSelected = false, + afterListItemAction = {}, + showSelectableItemAfter = true, + ) } } }