Skip to content

[Due for payment 2026-10-18] [$1000] [Tracking] SelectionList ListItem - Composition over Configuration #97971

Closed
7 of 7 issues completed

Description

@melvin-bot

Composition over configuration: SelectionList ListItem

The ListItem layer under SelectionList carries 33 unique boolean props (47 declarations; 14 are re-declared across the stacked prop types) and is rendered on 155 call sites, spread across two list components (SelectionList and SelectionListWithSections).

Since we're actively decomposing large components in the codebase, it's a solid next candidate — the 15 live variants (UserListItem, InviteMemberListItem, CardListItem, …) copy-paste the same content recipes over and over (title 7×, alternate text 6×, avatar-with-focus/hover-border 2×, rightElement + focus context 2×, right caret 3×), and the Search result rows (TaskListItem, ChatListItem) each hand-roll their own animated-highlight style bundle.

Proposed direction

The interface should be granular (flexibility for the custom rows — Search results, split editors), with the existing variants acting as presets covering most of the app. The 155 call sites need zero code changes — variants just become thin wiring over shared primitives, so every styling/a11y fix lands once and reaches all of them.

State sharing works like the decomposed Button (see #83762): hover/focus/selection reaches primitives through row context, which is also what lets the render-prop children go away. Sketch:

// Preset — the ~90% case: pages keep passing a variant, completely unchanged
<SelectionList ListItem={UserListItem} ... />

// Granular — full control for custom rows (e.g. Search results)
<ListItem.Pressable item={item} onSelectRow={onSelectRow}>
    <ListItem.Row style={rowStyle}>
        <ListItem.Avatar reportID={item.reportID} />
        <ListItem.TextColumn>
            <ListItem.Title text={item.text} />
            <ListItem.Subtitle text={item.alternateText} />
        </ListItem.TextColumn>
        <ListItem.RBRIndicator brickRoadIndicator={item.brickRoadIndicator} />
        <ListItem.RightCaret />
    </ListItem.Row>
</ListItem.Pressable>

// Style bundle — replaces the ~18 lines of copied highlight wiring per Search row with one hook call
const {pressableStyle, pressableWrapperStyle} = useListItemHighlight({shouldHighlight, isSelected, variant: 'searchTable', isLargeScreenWidth, isLastItem});

The existing <BaseListItem> / <SelectableListItem> APIs stay as-is during the migration (eventually they become thin adapters over the composed core), so nothing has to move at once. The prop surface shrinks along the way (what survives is item data, not config), and follow-ups can cut 1–3 boilerplate props from most call sites.

Related: #83762 (Button — Composition over Configuration)

cc @mountiny @roryabraham

Reported in Slack.

Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. changed the title [-][Tracking] SelectionList ListItem - Composition over Configuration[/-] [+][$1000] [Tracking] SelectionList ListItem - Composition over Configuration[/+] on Aug 6, 2026
  2. melvin-bot commented on Aug 6, 2026

    @melvin-bot
    Author

    ⚠️ Could not update price automatically because there is no linked Upwork Job ID. The BZ team member will need to update the price manually in Upwork.

  3. mountiny commented on Aug 6, 2026

    @mountiny
    Contributor
  4. jmusial commented on Aug 11, 2026

    @jmusial
    Contributor

    @mountiny PR split for sub issues. After PR 6 I'll reassess and there should be couple PRs of cleaning up redundant props

    PR 1 — Core + SearchQueryListItem

    • Migrates: SearchQueryListItem.
    • Components needed: ListItem.Pressable, Row, Title, Subtitle, RBRIndicator, ListItemFocusContext + useListItemFocus.
    • Implementation details:
      • Pressable = OfflineWithFeedback + PressableWithFeedback, press/hover/focus/a11y, zero layout; a11y extracted to utils/getListItemAccessibilityProps.ts.
      • Widen ListItemFocusContext to {isFocused?, isFocusVisible?, isHovered?} and switch MemberRightIcon.
      • Side quests: extract useListItemHighlight and convert TaskListItem, ChatListItem; delete TableListItem (dead code, referenced only by types.ts).

    PR 2 — SelectableListItem adapter + BaseSelectListItem + TravelDomainListItem + CardListItem

    • Migrates: BaseSelectListItem / SingleSelectListItem, TravelDomainListItem, CardListItem. SingleSelectWithAvatarListItem.
    • Components needed: SelectableListItem adapter (checkbox/radio via ListSelectionButton, role derived from canSelectMultiple, composed on ListItem.Pressable); TextColumn.
    • Deletes: components/ListCheckbox.tsx, components/ListRadioButton.tsx — pure role-fixing wrappers; callers pass role={CONST.ROLE.CHECKBOX} / {CONST.ROLE.RADIO} on ListSelectionButton directly.
    • Implementation details:
      • Multiline: styles.preWrap + inline {paddingLeft}; muted: styles.colorMuted; subtitle maxWidth via style.

    PR 3 — CompactAvatar + multi-select rows

    • Migrates: MultiSelectListItem, UserSelectionListItem.
    • Components needed: CompactAvatar.
    • Implementation details:
      • Renders pre-resolved item.icons entry; AVATAR_SIZE.X_SMALL, mention-suggestion container styles.

    PR 4 — Avatar resolvers + UserListItem family

    • Migrates: UserListItemContent, UserListItem, BareUserListItem, InviteMemberListItem.
    • Components needed: ReportAvatar, UserAvatar, WorkspaceAvatar, RightCaret.
    • Implementation details:
      • item.rightElement renders as plain child; no RightElement primitive.
      • UserListItemContent drops isFocused, isFocusVisible, hovered, shouldDisableHoverStyle props; hosts pass it as plain child instead of render-prop.
      • Drift fixes: avatar focus border via context isFocusVisible; caret hover fill via getButtonState, gated by shouldDisableHoverStyle through context; MemberRightIcon badge on visual focus.

    PR 5 — Sweep and flag deletion

    • Migrates: the full residue, enumerated — SpendCategorySelectorListItem, SpendRuleListItem, SplitListItem (the three remaining direct BaseListItem wrappers). Nothing else is left: every other ValidListItem entry lands in PRs 1-5 or is covered transitively.
    • Components needed: none new.
    • Implementation details:
      • SplitListItem migrates its pressable/layout like any variant; only its highlight stays on raw useAnimatedHighlightStyle.
      • Each flag dies with its last consumer: shouldDisplayRBR, prop-level shouldShowRightCaret (item-level flag from useConfirmationSections.ts stays), rightHandSideComponent, FooterComponent, wrapperStyle/testID forwarding.
      • ListItem/types.ts reduction; drop render-prop compat.

    PR 6 — Search-row highlight conversions

    • Migrates: nothing — highlight wiring only.
    • Converts: TransactionListItemWide, TransactionListItemNarrow, TransactionGroupListItem, ExpenseReportListItem.
    • Components needed: three new useListItemHighlight params. (It depends on PR 1 alone and can land any time after it.)
    • Implementation details:
      • These four are Search rows with no relation to the SelectionList
  5. mountiny commented on Aug 11, 2026

    @mountiny
    Contributor

    I will let @roryabraham handle this 🙌

  6. roryabraham commented on Aug 11, 2026

    @roryabraham
    Contributor

    @MelvinBot create sub-issues for each of the PR 6 phases @jmusial listed in his previous comment. Assign each to @jmusial, @truph01, and myself @roryabraham. Label the issues for PRs 1-2 Daily, 3-6 Weekly

  7. 25 remaining items

  8. roryabraham commented on Oct 6, 2026

    @roryabraham
    Contributor

    @MelvinBot post a status update / progress report on this issue, and on each of the sub-issues

  9. MelvinBot commented on Oct 6, 2026

    @MelvinBot
    Contributor

    Status update — 2026-10-06

    All 7 PRs are merged. Phases 1–6 are in production and past their 7-day regression windows. The last one, the dead-props cleanup, went to staging yesterday in 9.5.3-0 and should reach production with the next release. When that ships, the project can move to payment.

    Phase PR Merged Status
    PR 1 Add composed ListItem core and migrate SearchQueryListItem Aug 24 ✅ In production, issue closed
    PR 2 SelectableListItem adapter + BaseSelectListItem + TravelDomainListItem + CardListItem Sep 1 ✅ In production, issue closed
    PR 3 CompactAvatar + multi-select rows Sep 4 ✅ In production, issue closed
    PR 4 User list item decomposition Sep 16 ✅ Production Sep 17, regression window over, issue open
    PR 5 SelectionList ListItem decomposition - sweep and flag deletion Sep 21 ✅ Production Sep 24, regression window over, issue open
    PR 6 SelectionList ListItem decomposition - Search-row highlight conversions Sep 23 ✅ Production Sep 25, regression window over, issue open
    Cleanup Chore/remove dead list item props Oct 5 🚧 On staging (9.5.3-0)

    No regressions attributed to this project so far. Four deploy blockers were auto-linked to sub-issues, but the investigations traced each one to an unrelated PR:

    Next steps

    1. Close the PR 4, PR 5, and PR 6 sub-issues. Their work is in production and past regression. Automation is still flagging PR 5 and PR 6 as Overdue.
    2. Watch the cleanup PR through the production deploy and its 7-day regression window.
    3. Then add Awaiting Payment here with the $1,000 total, as mallenexpensify asked.
    4. Clean up payment labels on the sub-issues. PR 1 and PR 2 still have stale Awaiting Payment labels. PR 3 and the cleanup issue don't have Skip Payment.

    view run

  10. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    on Oct 18, 2026
  11. changed the title [-][$1000] [Tracking] SelectionList ListItem - Composition over Configuration[/-] [+][Due for payment 2026-10-18] [$1000] [Tracking] SelectionList ListItem - Composition over Configuration[/+] on Oct 18, 2026
  12. melvin-bot commented on Oct 18, 2026

    @melvin-bot
    Author

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  13. truph01 commented on Oct 18, 2026

    @truph01
    Contributor

    @mallenexpensify It is ready for payment and the total amount is $1000

  14. melvin-bot commented on Oct 18, 2026

    @melvin-bot
    Author

    Payment Summary

    • Contributor: @jmusial does not require payment (Contractor)
    • Reviewer: @truph01 owed $1000 via NewDot

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  15. mallenexpensify commented on Oct 19, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @truph01 due $1000 via NewDot

    Guessing that's correct and that we don't need anything else, inc. test steps. cc @roryabraham

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionTaskWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions