@@ -315,6 +315,243 @@ func TestListMovement(t *testing.T) {
315315 assert.Equal(t, 0, l.offset)
316316 golden.RequireEqual(t, []byte(l.View()))
317317 })
318+
319+ t.Run("should not change offset when new items are appended and we are at the bottom in backwards list", func(t *testing.T) {
320+ t.Parallel()
321+ items := []Item{}
322+ for i := range 30 {
323+ content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
324+ content = strings.TrimSuffix(content, "\n")
325+ item := NewSelectableItem(content)
326+ items = append(items, item)
327+ }
328+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
329+ execCmd(l, l.Init())
330+ execCmd(l, l.AppendItem(NewSelectableItem("Testing")))
331+
332+ assert.Equal(t, 0, l.offset)
333+ golden.RequireEqual(t, []byte(l.View()))
334+ })
335+
336+ t.Run("should stay at the position it is when new items are added but we moved up in backwards list", func(t *testing.T) {
337+ t.Parallel()
338+ items := []Item{}
339+ for i := range 30 {
340+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
341+ items = append(items, item)
342+ }
343+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
344+ execCmd(l, l.Init())
345+
346+ execCmd(l, l.MoveUp(2))
347+ viewBefore := l.View()
348+ execCmd(l, l.AppendItem(NewSelectableItem("Testing\nHello\n")))
349+ viewAfter := l.View()
350+ assert.Equal(t, viewBefore, viewAfter)
351+ assert.Equal(t, 5, l.offset)
352+ assert.Equal(t, 33, lipgloss.Height(l.rendered))
353+ golden.RequireEqual(t, []byte(l.View()))
354+ })
355+ t.Run("should stay at the position it is when the hight of an item below is increased in backwards list", func(t *testing.T) {
356+ t.Parallel()
357+ items := []Item{}
358+ for i := range 30 {
359+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
360+ items = append(items, item)
361+ }
362+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
363+ execCmd(l, l.Init())
364+
365+ execCmd(l, l.MoveUp(2))
366+ viewBefore := l.View()
367+ item := items[29]
368+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
369+ viewAfter := l.View()
370+ assert.Equal(t, viewBefore, viewAfter)
371+ assert.Equal(t, 4, l.offset)
372+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
373+ golden.RequireEqual(t, []byte(l.View()))
374+ })
375+ t.Run("should stay at the position it is when the hight of an item below is decreases in backwards list", func(t *testing.T) {
376+ t.Parallel()
377+ items := []Item{}
378+ for i := range 30 {
379+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
380+ items = append(items, item)
381+ }
382+ items = append(items, NewSelectableItem("Item 30\nLine 2\nLine 3"))
383+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
384+ execCmd(l, l.Init())
385+
386+ execCmd(l, l.MoveUp(2))
387+ viewBefore := l.View()
388+ item := items[30]
389+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 30")))
390+ viewAfter := l.View()
391+ assert.Equal(t, viewBefore, viewAfter)
392+ assert.Equal(t, 0, l.offset)
393+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
394+ golden.RequireEqual(t, []byte(l.View()))
395+ })
396+ t.Run("should stay at the position it is when the hight of an item above is increased in backwards list", func(t *testing.T) {
397+ t.Parallel()
398+ items := []Item{}
399+ for i := range 30 {
400+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
401+ items = append(items, item)
402+ }
403+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
404+ execCmd(l, l.Init())
405+
406+ execCmd(l, l.MoveUp(2))
407+ viewBefore := l.View()
408+ item := items[1]
409+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 1\nLine 2\nLine 3")))
410+ viewAfter := l.View()
411+ assert.Equal(t, viewBefore, viewAfter)
412+ assert.Equal(t, 2, l.offset)
413+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
414+ golden.RequireEqual(t, []byte(l.View()))
415+ })
416+ t.Run("should stay at the position it is if an item is prepended and we are in backwards list", func(t *testing.T) {
417+ t.Parallel()
418+ items := []Item{}
419+ for i := range 30 {
420+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
421+ items = append(items, item)
422+ }
423+ l := New(items, WithDirectionBackward(), WithSize(10, 10)).(*list[Item])
424+ execCmd(l, l.Init())
425+
426+ execCmd(l, l.MoveUp(2))
427+ viewBefore := l.View()
428+ execCmd(l, l.PrependItem(NewSelectableItem("New")))
429+ viewAfter := l.View()
430+ assert.Equal(t, viewBefore, viewAfter)
431+ assert.Equal(t, 2, l.offset)
432+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
433+ golden.RequireEqual(t, []byte(l.View()))
434+ })
435+
436+ t.Run("should not change offset when new items are prepended and we are at the top in forward list", func(t *testing.T) {
437+ t.Parallel()
438+ items := []Item{}
439+ for i := range 30 {
440+ content := strings.Repeat(fmt.Sprintf("Item %d\n", i), i+1)
441+ content = strings.TrimSuffix(content, "\n")
442+ item := NewSelectableItem(content)
443+ items = append(items, item)
444+ }
445+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
446+ execCmd(l, l.Init())
447+ execCmd(l, l.PrependItem(NewSelectableItem("Testing")))
448+
449+ assert.Equal(t, 0, l.offset)
450+ golden.RequireEqual(t, []byte(l.View()))
451+ })
452+
453+ t.Run("should stay at the position it is when new items are added but we moved down in forward list", func(t *testing.T) {
454+ t.Parallel()
455+ items := []Item{}
456+ for i := range 30 {
457+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
458+ items = append(items, item)
459+ }
460+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
461+ execCmd(l, l.Init())
462+
463+ execCmd(l, l.MoveDown(2))
464+ viewBefore := l.View()
465+ execCmd(l, l.PrependItem(NewSelectableItem("Testing\nHello\n")))
466+ viewAfter := l.View()
467+ assert.Equal(t, viewBefore, viewAfter)
468+ assert.Equal(t, 5, l.offset)
469+ assert.Equal(t, 33, lipgloss.Height(l.rendered))
470+ golden.RequireEqual(t, []byte(l.View()))
471+ })
472+
473+ t.Run("should stay at the position it is when the hight of an item above is increased in forward list", func(t *testing.T) {
474+ t.Parallel()
475+ items := []Item{}
476+ for i := range 30 {
477+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
478+ items = append(items, item)
479+ }
480+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
481+ execCmd(l, l.Init())
482+
483+ execCmd(l, l.MoveDown(2))
484+ viewBefore := l.View()
485+ item := items[0]
486+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
487+ viewAfter := l.View()
488+ assert.Equal(t, viewBefore, viewAfter)
489+ assert.Equal(t, 4, l.offset)
490+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
491+ golden.RequireEqual(t, []byte(l.View()))
492+ })
493+
494+ t.Run("should stay at the position it is when the hight of an item above is decreases in forward list", func(t *testing.T) {
495+ t.Parallel()
496+ items := []Item{}
497+ items = append(items, NewSelectableItem("At top\nLine 2\nLine 3"))
498+ for i := range 30 {
499+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
500+ items = append(items, item)
501+ }
502+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
503+ execCmd(l, l.Init())
504+
505+ execCmd(l, l.MoveDown(3))
506+ viewBefore := l.View()
507+ item := items[0]
508+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("At top")))
509+ viewAfter := l.View()
510+ assert.Equal(t, viewBefore, viewAfter)
511+ assert.Equal(t, 1, l.offset)
512+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
513+ golden.RequireEqual(t, []byte(l.View()))
514+ })
515+
516+ t.Run("should stay at the position it is when the hight of an item below is increased in forward list", func(t *testing.T) {
517+ t.Parallel()
518+ items := []Item{}
519+ for i := range 30 {
520+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
521+ items = append(items, item)
522+ }
523+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
524+ execCmd(l, l.Init())
525+
526+ execCmd(l, l.MoveDown(2))
527+ viewBefore := l.View()
528+ item := items[29]
529+ execCmd(l, l.UpdateItem(item.ID(), NewSelectableItem("Item 29\nLine 2\nLine 3")))
530+ viewAfter := l.View()
531+ assert.Equal(t, viewBefore, viewAfter)
532+ assert.Equal(t, 2, l.offset)
533+ assert.Equal(t, 32, lipgloss.Height(l.rendered))
534+ golden.RequireEqual(t, []byte(l.View()))
535+ })
536+ t.Run("should stay at the position it is if an item is appended and we are in forward list", func(t *testing.T) {
537+ t.Parallel()
538+ items := []Item{}
539+ for i := range 30 {
540+ item := NewSelectableItem(fmt.Sprintf("Item %d", i))
541+ items = append(items, item)
542+ }
543+ l := New(items, WithDirectionForward(), WithSize(10, 10)).(*list[Item])
544+ execCmd(l, l.Init())
545+
546+ execCmd(l, l.MoveDown(2))
547+ viewBefore := l.View()
548+ execCmd(l, l.AppendItem(NewSelectableItem("New")))
549+ viewAfter := l.View()
550+ assert.Equal(t, viewBefore, viewAfter)
551+ assert.Equal(t, 2, l.offset)
552+ assert.Equal(t, 31, lipgloss.Height(l.rendered))
553+ golden.RequireEqual(t, []byte(l.View()))
554+ })
318555}
319556
320557type SelectableItem interface {