Skip to content

Add Compose list item family (one line, two line, settings) - #9249

Open
mikescamell wants to merge 6 commits into
developfrom
feature/mike/compose/list-items
Open

Add Compose list item family (one line, two line, settings)#9249
mikescamell wants to merge 6 commits into
developfrom
feature/mike/compose/list-items

Conversation

@mikescamell

@mikescamell mikescamell commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Task/Issue URL: https://app.asana.com/1/137249556945/project/1215496415658080/task/1211659112661214?focus=true
Tech Design URL (if applicable):

Description

Adds the Compose list item family to the design system, following on from the buttons and text inputs.

  • DaxOneLineListItem, DaxTwoLineListItem and DaxSettingsListItem. All are thin variants over an internal DaxListItem core with receiver-scoped leading/trailing slots, so callers can only place content the design system allows
  • Leading icons support size and background independently, with an optional tint. Trailing content supports decorative or clickable icons, a switch, a button and a status indicator. There's also an optional inline pill via pillText
  • DaxPill, a standalone status pill matching the View DaxYellowPill. The caller supplies the (localised) label so the design system owns no product copy
  • Two new lint rules: one keeps the leading/trailing slots to their scoped composables, the other requires primaryTextColor/secondaryTextColor to resolve to DuckDuckGoTheme semantic colours
  • The internal ADS showcase now shows each View list item next to its Compose equivalent, captioned "View" and "Compose". Rows the Compose API intentionally does not cover are flagged "View (no Compose equivalent)"
  • A new PreviewSurface for full-width components that carry their own internal padding
  • A Maestro test asserts the new showcase sections

The intentional divergences from the View components and Figma are documented in the module README.

Steps to test this PR

List items showcase

  • Install an internal debug build
  • Go to Settings -> Android Design System Preview -> LIST ITEMS
  • In the Single Line Items, Two Line Items and Settings sections, check each Compose row renders the same as the View row above it, in light and dark mode
  • Check the switches and trailing icons in the Compose rows respond to taps

Lint rules

  • Run ./gradlew :lint-rules:test and check the new detector tests pass

Maestro

  • Run maestro test .maestro/ads_preview_flows/1-_design-system-components.yaml with the app installed

UI changes

Before After
!(Upload before screenshot) (Upload after screenshot)

Note

Medium Risk
New public design-system APIs and a breaking reshape of DaxOneLineListItem; production impact is small (one dialog call site updated) but adopters must migrate parameters and respect new lint rules.

Overview
Introduces Compose list items (DaxOneLineListItem, DaxTwoLineListItem, DaxSettingsListItem) on a shared internal DaxListItem, with receiver-scoped leadingContent / trailingContent (icons, switch, button, status indicator) and optional pillText via new DaxPill.

DaxOneLineListItem is reworked from the prior Material3 ListItem stub to the design-system API (primaryText, slots, pills, annotated text overloads). DialogsFragment is updated to that API.

The internal ADS showcase interleaves each View row with a matching Compose ComposeView, adds View / Compose / no-equivalent captions, passes isDarkTheme into list-item view holders, and refactors list layouts from ConstraintLayout to LinearLayout. PreviewSurface is added for full-width previews.

Lint registers DaxListItemContentDetector and DaxListItemColorUsageDetector (with tests). Maestro scrolls/asserts Single Line, Two Line, and Settings showcase sections.

Reviewed by Cursor Bugbot for commit bc35e80. Bugbot is set up for automated code reviews on this repo. Configure here.

A standalone, label-agnostic pill for short status labels (e.g. Beta/New). Fixed DuckDuckGo-yellow palette in light and dark, matching the View DaxYellowPill; the caller supplies the (localised) label so the design system owns no product copy.
… to PreviewContainers

PreviewSurface renders on a DaxSurface with no padding, for full-width components (list items) that carry their own internal padding and would look double-inset in the padded PreviewBox. The file is renamed to PreviewContainers now it holds more than a box.

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@mikescamell mikescamell changed the title feat(ads-compose): add DaxPill status pill Add Compose list item family (one line, two line, settings) Jul 21, 2026
@mikescamell
mikescamell force-pushed the feature/mike/compose/list-items branch from 8ceb026 to 3174581 Compare July 21, 2026 09:17
Adds DaxOneLineListItem, DaxTwoLineListItem and DaxSettingsListItem as thin public variants over an internal DaxListItem core with receiver-scoped leading/trailing slots, plus a DaxListItemDefaults object (PascalCase constants).

Text params are runtime-stable: typed String / AnnotatedString public overloads funnel into a single AnnotatedString-typed core, so the composables stay skippable. Leading icons expose independent size and background axes with an optional tint; trailing content supports decorative or clickable icons, switch, button and status indicator. Optional inline pill via pillText. README documents the intentional View/Figma divergences.
DaxListItemContentDetector keeps the leading/trailing slots to their receiver-scope composables (resolved dynamically, so it needs no update as scope members change). DaxListItemColorUsageDetector requires primaryTextColor/secondaryTextColor to resolve to DuckDuckGoTheme semantic colours. Both registered in DuckDuckGoIssueRegistry, with tests.
@mikescamell
mikescamell force-pushed the feature/mike/compose/list-items branch from 3174581 to d6208f2 Compare July 21, 2026 09:25
@mikescamell
mikescamell force-pushed the feature/mike/compose/list-items branch from d6208f2 to 753925d Compare July 21, 2026 09:39
@mikescamell
mikescamell force-pushed the feature/mike/compose/list-items branch from 753925d to 73d85e3 Compare July 21, 2026 09:45
@mikescamell
mikescamell force-pushed the feature/mike/compose/list-items branch from 73d85e3 to f5f5a70 Compare July 21, 2026 09:50

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f5f5a70. Configure here.

Reworks the one-line, two-line and settings list-item showcase screens to present each legacy View row beside its Compose equivalent (DaxOneLineListItem, DaxTwoLineListItem, DaxSettingsListItem), captioned "View" and "Compose". Rows the Compose API intentionally does not cover (16dp small and 40dp extra-large leading icons, rounded icon background) are flagged "View (no Compose equivalent)".

Converts the three showcase layouts from ConstraintLayout to LinearLayout, drives the interleaved ComposeView slots from ComponentViewHolder, and removes the standalone component_compose_list_item.xml now that the demos live in the per-type layouts. Caption strings are added to a new design-system-internal donottranslate.xml.

Also migrates the DialogsFragment call-sites to the new DaxOneLineListItem API (text -> primaryText, leadingIcon -> scoped leadingContent).
Scrolls to and asserts the three Compose list-item section headers and representative rows in the ADS components flow, in lockstep with the showcase strings.
@mikescamell
mikescamell force-pushed the feature/mike/compose/list-items branch from f5f5a70 to bc35e80 Compare July 21, 2026 09:59
@mikescamell
mikescamell marked this pull request as ready for review July 21, 2026 10:21
@mikescamell
mikescamell requested a review from malmstein as a code owner July 21, 2026 10:21
Comment on lines +30 to +37
return listOf(
SECTION_HEADER_LIST_ITEM,
SINGLE_LINE_LIST_ITEM,
TWO_LINE_LIST_ITEM,
SETTINGS_LIST_ITEM,
MENU_ITEM,
POPUP_MENU_ITEM,
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this re-formatted?

@landomen landomen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few non-blocking comments otherwise it looks good

modifier = modifier
.height(DaxPillDefaults.Height)
.clip(RoundedCornerShape(DaxPillDefaults.CornerRadius))
.background(Yellow50)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses Yellow50 directly, but should we go through DuckDuckGoPillColors to match other components?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree, same for Text color.

contentAlignment = Alignment.Center,
) {
Text(
text = text.uppercase(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could in theory be the callers responsibility, but considering existing DaxYellowPill already does this it's fine.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All examples of Pill in the design system use capitalisation, so I'd like to enforce it and if something changes, we can update it 👍

* Fixed palette in both light and dark (DuckDuckGo yellow background, black text) — it is not
* theme-tokened and draws its own text colour, matching the View [DaxYellowPill]. Callers supply
* their own (localised) label.
*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Figma and Asana links

* The primary label defaults to a single ellipsised line; the secondary caption is unbounded (wraps),
* matching the View. Sibling overloads accept [AnnotatedString] for HTML / inline-styled text.
*
* Figma reference: https://www.figma.com/design/BOHDESHODUXK7wSRNBOHdu/%F0%9F%A4%96-Android-Components?node-id=6032-13665

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Asana link like other components have

modifier: Modifier = Modifier,
primaryTextColor: Color = DuckDuckGoTheme.textColors.primary,
secondaryTextColor: Color = DuckDuckGoTheme.textColors.secondary,
pillText: String? = null,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to expose this as a slot where callers put DaxPill vs a String?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is a good point, does it make sense to make this slottable and do something similar to DaxListItemTrailingScope to enforce that for now we can only add a DaxPill?

Comment on lines +61 to +62
primaryText: String,
secondaryText: String,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to expose this as a slot where callers put DaxText vs a String? That also removes the need for primaryTextColor and secondaryTextColor.

I get the benefit of keeping it like now, it also matches existing XML. Moving to a more slot-based component makes it more Compose-like, but also adds more work for the caller and enables invalid usages (for which we'd need more lint rules).

I'm okay either way, just want to open a discussion since we went with more open slot approach for some other components.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe there are any usages of ListItem's without text, therefore I think I prefer to lock it down and only allow Strings to enforce the API without the need of Lint. If at some point we need a ListItem that at it's core does not have text, then I think it would be appropriate to move to slots as you mention 👍

* Settings row: one-line list item whose trailing element is a status indicator.
*
* The trailing slot is fixed to a [DaxListItemTrailingScope.StatusIndicator] driven by [status]; it is not a free slot like the other variants.
*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Figma and Asana links

var checked by remember { mutableStateOf(false) }
DaxTwoLineListItem(
primaryText = "Two Line Item Two Line Item Two Line Item Two Line Item",
secondaryText = "In disabled state",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this one is not in a disabled state since there's no enabled=false 🕵️

import com.duckduckgo.common.ui.compose.theme.DuckDuckGoTheme

@Composable
internal fun DaxListItem(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add KDocs for this composable as well, even if it’s internal

verticalAlignment = Alignment.CenterVertically,
) {
if (leadingContent != null) {
leadingScope.leadingContent()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the way to enforce that the leading content is part of DaxListItemLeadingScope, right?

}

@Stable
class DaxListItemLeadingScope internal constructor(private val parentEnabled: Boolean) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add KDocs here as well?

}

@Stable
class DaxListItemTrailingScope internal constructor(private val parentEnabled: Boolean) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add KDocs?

*/
@Composable
fun DaxTwoLineListItem(
primaryText: AnnotatedString,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just making sure, we don’t need a variant with primary text as AnnotatedString and secondary text as String, right? From a design perspective I don’t think it makes sense but just want to make sure that we didn’t miss this component variant.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah are there any examples of this in the codebase where the current View version's primary text needs an Annotated string?

@catalinradoiu catalinradoiu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some small comments, but overall it looks good to me.

}
val leadingScope = DaxListItemLeadingScope(enabled)
val trailingScope = DaxListItemTrailingScope(enabled)
Row(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space between Row and declared properties. It would be easier to read if there were. Is there anything we could apply to Spotless to enforce this?

.alpha(if (effectiveEnabled) 1f else DaxListItemDefaults.DisabledAlpha),
)
} else {
androidx.compose.material3.Icon(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have the full import here? It should just be Icon

leadingScope.leadingContent()
Spacer(Modifier.width(DaxListItemDefaults.LeadingGap))
}
Column(Modifier.weight(1f).alpha(if (enabled) 1f else DaxListItemDefaults.DisabledAlpha)) {

@mikescamell mikescamell Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space between Column and declared properties. It would be easier to read if there were. Is there anything we could apply to Spotless to enforce this?

DaxPill(text = pillText)
}
}
if (secondaryText != null) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space between the if statement and previous Row code.

)
}
}
if (trailingContent != null) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space between the if statement and previous code.

DaxListItemTrailingIconSize.Small -> DaxListItemDefaults.TrailingIconSmall
DaxListItemTrailingIconSize.Medium -> DaxListItemDefaults.TrailingIconMedium
}
if (onClick != null) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space between the if statement and previous code.

@0nko 0nko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went over the code and aside from the existing comments I didn't find any issues. Nice work! 🏅

When I was looking at the View vs. Compose differences, I noticed some UI and behavior differences, like:

  • Different leading icon sizes and color (globe) (smaller in Compose)
  • Different ripple size when tapping the menu icon (smaller in Compose)
  • No leading icon click handler in Compose
  • Smaller padding between primary and secondary text in Compose

I was looking for the documentation for the differences but I couldn't find any README and this is empty. Is it somewhere else?

@duckduckgo duckduckgo deleted a comment from 0nko Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants