Skip to content

fix: PrevItem does not scroll viewport at top boundary#838

Closed
lawrence3699 wants to merge 1 commit intodlvhdr:mainfrom
lawrence3699:fix/previtem-viewport-scroll
Closed

fix: PrevItem does not scroll viewport at top boundary#838
lawrence3699 wants to merge 1 commit intodlvhdr:mainfrom
lawrence3699:fix/previtem-viewport-scroll

Conversation

@lawrence3699
Copy link
Copy Markdown

Bug: When navigating up through a list that extends beyond one page, the viewport fails to scroll when the cursor reaches the topmost visible row. The cursor moves above the visible area, becoming invisible.

Cause: PrevItem uses < to check if the cursor is at the top of the viewport, but the symmetric NextItem correctly uses >= for the bottom boundary. Since currId is always >= topBoundId during normal step-by-step navigation, the < check never triggers when the cursor is at the top boundary.

Before: Cursor at row 1, topBoundId=1. Press up → cursor moves to row 0, viewport stays at topBoundId=1. Row 0 is above the visible area.

After: Same scenario → viewport scrolls up to topBoundId=0, cursor at row 0 is visible.

Fix: Change the check to > 0 && <= — the <= matches NextItem's >= pattern, and the > 0 guard prevents an unnecessary scroll when already at the first item.

Validation: Added regression tests for PrevItem/NextItem boundary scrolling and edge cases. go test ./... and go build ./... both pass.

AI Disclosure: Claude Code was used to identify the asymmetry and draft the fix and tests. I (the submitter) fully understand the off-by-one: NextItem scrolls when currId >= bottomBoundId (at or past the bottom row), but PrevItem only scrolled when currId < topBoundId (already past the top row, which doesn't happen in single-step navigation). The <= makes both directions scroll when the cursor is at the boundary.

PrevItem used `<` to check if the cursor was at the top of the viewport,
but the symmetric NextItem correctly uses `>=` for the bottom boundary.
This meant navigating up when the cursor was at the topmost visible row
would move the cursor above the visible area without scrolling.

Change the check to `> 0 && <=` so the viewport scrolls when the cursor
is at the top bound (matching NextItem behavior), while the `> 0` guard
prevents an unnecessary scroll when already on the first item.
Copilot AI review requested due to automatic review settings April 15, 2026 11:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an off-by-one boundary condition in the TUI list viewport navigation so that moving upward scrolls the viewport when the cursor reaches the topmost visible row (symmetric with the existing downward behavior).

Changes:

  • Adjust PrevItem top-boundary detection to trigger scrolling when the cursor is at (or above) the top viewport boundary, while guarding against scrolling at item 0.
  • Add regression tests covering PrevItem top-boundary scrolling, NextItem bottom-boundary scrolling, and first/last item edge behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
internal/tui/components/listviewport/listviewport.go Fixes PrevItem boundary check to scroll when the cursor is at the top visible row.
internal/tui/components/listviewport/listviewport_test.go Adds regression tests for up/down boundary scrolling and navigation edge cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Owner

@dlvhdr dlvhdr left a comment

Choose a reason for hiding this comment

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

thanks for this! has been bugging me.
and also thx for adding tests!


func (m *Model) PrevItem() int {
atTopOfViewport := m.currId < m.topBoundId
atTopOfViewport := m.currId > 0 && m.currId <= m.topBoundId
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I don't really get the need for the m.currId > 0 condition. I tested it without it and it seems to work fine. Also the atTopOfViewport is misleading with that condition.

func TestPrevItemScrollsAtTopBound(t *testing.T) {
// 10 items, viewport height 5, item height 1 => 5 items per page
// Initial bounds: topBoundId=0, bottomBoundId=4
m := newTestModel(10, 5, 1)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

can we add a struct for the arguements? makes it clearer

newTestModel(opts testModelOpts) Model {
  // ...
}


// Navigate back up to the top bound
// 4 PrevItem calls: currId goes 4, 3, 2, 1
for i := 0; i < 4; i++ {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

maybe the 4 can be reused as opts.viewportHeight - 1?

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.

3 participants