Skip to content

fix(inverted): align ItemSeparatorComponent's trailingItem with visual flow#2301

Open
pranko17 wants to merge 1 commit into
Shopify:mainfrom
pranko17:fix/inverted-separator-trailing-item
Open

fix(inverted): align ItemSeparatorComponent's trailingItem with visual flow#2301
pranko17 wants to merge 1 commit into
Shopify:mainfrom
pranko17:fix/inverted-separator-trailing-item

Conversation

@pranko17
Copy link
Copy Markdown

Summary

ItemSeparatorComponent is documented to receive { leadingItem, trailingItem } so consumers can render context-aware separators (think: "small gap between same-sender chat bubbles, larger gap on sender switch"). In the default layout the prop names match the visual flow — leadingItem is the cell above the separator, trailingItem is the cell below.

In inverted mode this assumption breaks: cells are positioned in reverse visual order (array index 0 is at the visual bottom), so the separator owned by cell i visually appears between data[i] (above) and data[i - 1] (below). The current code still passes data[index + 1] as trailingItem, which is the cell two rows away — not the visually-adjacent neighbor below the separator. Anyone comparing leadingItem vs trailingItem in an inverted list ends up looking at the wrong pair.

This is most painful in chat UIs (inverted is the canonical layout for chronological chat lists). Imagine bubbles [user, user, expert] in chronological reading. With newest-first data + inverted, the array is [expert, user, user]. The separator inside the middle cell (leading=user, trailing=user via data[i+1]) says "same sender → 4px gap". But the visually-adjacent items around that separator are the top user bubble and the expert bubble — different senders, should be 24px. The result is a 4px gap where the user expects 24px.

Fix

In ViewHolderCollection.tsx, branch on inverted:

// Before
const trailingItem =
  ItemSeparatorComponent && !isInLastRow(index)
    ? data[index + 1]
    : undefined;

// After
const trailingItem = ItemSeparatorComponent
  ? inverted
    ? index > 0
      ? data[index - 1]
      : undefined
    : !isInLastRow(index)
      ? data[index + 1]
      : undefined
  : undefined;
  • Inverted mode: trailingItem = data[index - 1] so it stays semantically aligned with the visually-adjacent item below the separator.
  • Inverted mode: skip the separator at index === 0 (visual-bottom cell has no neighbor below it). This mirrors the existing !isInLastRow(index) guard for the default case.
  • Non-inverted behavior is unchanged.

Tests

Added two cases in src/__tests__/RecyclerView.test.tsx → "Item separators in inverted mode":

  1. passes the visually-adjacent item as trailingItem in inverted mode — renders an inverted list with data=[10, 20, 30] and asserts the separators carry { leading: 20, trailing: 10 } and { leading: 30, trailing: 20 } (the visually-adjacent pair below each separator). Without the fix this would assert wrong values.
  2. does not render a separator below the visual-bottom item (index 0) — renders data=[10, 20] and asserts only one separator is rendered (between data[1] above and data[0] below); no separator dangling at the visual bottom.

All existing tests still pass (24 total in RecyclerView.test.tsx).

Notes

  • The behavioral change is opt-in (only triggers when inverted: true), so existing non-inverted consumers see no difference.
  • For inverted lists that previously got "wrong-pair" trailingItem values, this is a bug fix that may cause visible spacing changes in their separators. The new values match what most consumers actually intend by "between this cell and the next visible one."

Test plan

  • yarn test --testPathPattern=RecyclerView — 24/24 pass
  • yarn lint — clean

…l flow

`ItemSeparatorComponent` is passed `{ leadingItem: data[i], trailingItem:
data[i+1] }`, and in the default (non-inverted) layout `data[i+1]` is the
item visually below the separator — consumers can rely on `leadingItem`
being "above" and `trailingItem` being "below" the separator.

In `inverted` mode this breaks. Cells render in reverse visual order
(index 0 at the visual bottom), so each cell's separator visually
appears between `data[i]` (above) and `data[i - 1]` (below). The
existing code still passes `data[i + 1]` as `trailingItem`, which is
the item two cells away — not the visually-adjacent neighbor. A
consumer comparing `leadingItem` vs `trailingItem` (e.g. for
same-sender bubble grouping in a chat UI) ends up comparing the
wrong pair and renders wrong styling between bubbles.

Fix: in `inverted` mode pass `data[index - 1]` as `trailingItem`, and
skip the separator at `index === 0` (the visual-bottom cell has no
neighbor below it). Non-inverted behavior is unchanged.

Adds two tests covering the inverted case in
`src/__tests__/RecyclerView.test.tsx`.
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.

1 participant