Skip to content

fix(Virtual): place the row correctly for an initial or reactive selected - #83

Merged
chiefcll merged 1 commit into
mainfrom
fix/virtual-initial-selected
Nov 8, 2026
Merged

chiefcll merged 1 commit into
mainfrom
fix/virtual-initial-selected

Conversation

@chiefcll

@chiefcll chiefcll commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes three bugs that show up when VirtualRow/VirtualColumn gets a selected value (initially or reactively) or when scrollToIndex is called. All three are present in 1.7.1, and the new contract tests reproduce each one.

The first two match what a downstream app worked around with a pnpm patch on 1.6.6 (initial selected at the last item of an auto carousel, then Back). This PR fixes them here, so that patch can be removed after a release.

Bugs and fixes

1. scroll="auto": an initial selected near the end puts the focused item off-screen.
The initial-setup branch started the window at total - displaySize - bufferSize. With 12 items, displaySize={4} and selected={11}, the focused item landed at screen x 970, the 5th slot. The window now starts at total - displaySize, the same window navigation reaches at the end (8,9,10,11, focused item in slot 3).

2. Row ends up one slot off after selected / scrollToIndex.

  • updateSelected set targetPosition before shifting the row. The next key press shifts from targetPosition, so the first press after any initial selected moved the row one slot too far. The B14 contract test already called this "a bug from before 1.7 … (out of scope here)".
  • updateSelected also shifted from the row's current position, so a reactive selected change on a row that was already scrolled moved it a second slot (x -410 instead of -180).

updateSelected now places the row at originalPosition, minus one slot when the layout shifts. It sets targetPosition to where the row actually rests and stops a shift animation that is still running. The non-animated shift path now also updates targetPosition, so it never goes stale once animations are off.

Fixing only the first part would have made the reactive case worse. The old first-press jump happened to return the row to its base position, which hid the second-slot drift.

3. scroll="edge": an initial selected focuses the next item.
The initial window started at c - startScrolling + 1, one item too far (selected={7} focused v8). It now starts at c - selected.

Behaviour changes for reviewers

  • Two contract tests pinned the old first-press jump. Their expectations are updated:
    • In the B14 selected variant, the focused item's screen x is now [50, 50, 50, 50, 50, 280, …] instead of [50, 280, …, 510, …].
    • In the selected / reactive test, x stays at -180, and setSel(9) gives the end-of-list window 8,9,10,11.
  • A reactive selected change now also moves the row back to originalPosition when the new layout needs no shift. Before, the row stayed where it was, so selected={0} on a scrolled row could leave item 0 off-screen.
  • Not changed: rows with exactly displaySize items still take the non-windowed path (total <= displaySize). The downstream patch changed that to <, which makes auto rows scroll the first item off-screen and always rows stall before the last item. It's left as is here.

Tests

New contract tests in tests/contract-virtual.test.tsx:

  • auto with an initial selected of 11, 10, 9 and 5, at x=50 and x=0: the focused item is visible and keeps its screen x over Left×3 and Right.
  • auto / edge / always: a reactive selected on a scrolled row places the row exactly as the same initial selected does.
  • edge with an initial selected of 1, 3, 7 and 11 focuses that item.

Without the source changes, 12 tests fail. With them, the full suite passes: 755 passed, 13 skipped. The branch is rebased on #82.

Local npm run tsc still reports 4 errors in domRenderer.ts (atlasGroup, rtl on IRendererNodeProps). They come from the locally installed renderer version, appear on main as well, and are unrelated to this change.

🤖 Generated with Claude Code

…cted

Three bugs when VirtualRow/VirtualColumn is given `selected` (or
scrollToIndex is called), each reproduced by the new contract tests:

- scroll="auto": near the end of the list the window started at
  total - displaySize - bufferSize, so the focused item landed past the
  last visible slot (selected=11 of 12, displaySize 4: screen x 970).
  It now starts at total - displaySize, the window navigation reaches.
- updateSelected saved targetPosition before shifting the row, so the
  first press afterwards moved the row one slot too far. It also shifted
  from the current position, so a reactive selected on a scrolled row
  moved it a second slot. The row now goes to originalPosition (minus
  one slot when the layout shifts) and targetPosition is set to where it
  rests. The non-animated shift path also updates targetPosition.
- scroll="edge": the initial window started one item late, focusing
  item c + 1 (selected=7 focused v8).

Two contract tests pinned the old first-press jump; their expectations
are updated (one already called it a bug from before 1.7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chiefcll
chiefcll merged commit 60e7fb1 into main Nov 8, 2026
1 check passed
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