@@ -12,6 +12,93 @@ import (
1212 "github.com/stretchr/testify/assert"
1313)
1414
15+func TestListPosition(t *testing.T) {
16+ type positionOffsetTest struct {
17+ dir direction
18+ test string
19+ width int
20+ height int
21+ numItems int
22+
23+ moveUp int
24+ moveDown int
25+
26+ expectedStart int
27+ expectedEnd int
28+ }
29+ tests := []positionOffsetTest{
30+ {
31+ dir: Forward,
32+ test: "should have correct position initially when forward",
33+ moveUp: 0,
34+ moveDown: 0,
35+ width: 10,
36+ height: 20,
37+ numItems: 100,
38+ expectedStart: 0,
39+ expectedEnd: 19,
40+ },
41+ {
42+ dir: Forward,
43+ test: "should offset start and end by one when moving down by one",
44+ moveUp: 0,
45+ moveDown: 1,
46+ width: 10,
47+ height: 20,
48+ numItems: 100,
49+ expectedStart: 1,
50+ expectedEnd: 20,
51+ },
52+ {
53+ dir: Backward,
54+ test: "should have correct position initially when backward",
55+ moveUp: 0,
56+ moveDown: 0,
57+ width: 10,
58+ height: 20,
59+ numItems: 100,
60+ expectedStart: 80,
61+ expectedEnd: 99,
62+ },
63+ {
64+ dir: Backward,
65+ test: "should offset the start and end by one when moving up by one",
66+ moveUp: 1,
67+ moveDown: 0,
68+ width: 10,
69+ height: 20,
70+ numItems: 100,
71+ expectedStart: 79,
72+ expectedEnd: 98,
73+ },
74+ }
75+ for _, c := range tests {
76+ t.Run(c.test, func(t *testing.T) {
77+ l := New(WithDirection(c.dir)).(*list)
78+ l.SetSize(c.width, c.height)
79+ items := []Item{}
80+ for i := range c.numItems {
81+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
82+ items = append(items, item)
83+ }
84+ cmd := l.SetItems(items)
85+ if cmd != nil {
86+ cmd()
87+ }
88+
89+ if c.moveUp > 0 {
90+ l.MoveUp(c.moveUp)
91+ }
92+ if c.moveDown > 0 {
93+ l.MoveDown(c.moveDown)
94+ }
95+ start, end := l.viewPosition()
96+ assert.Equal(t, c.expectedStart, start)
97+ assert.Equal(t, c.expectedEnd, end)
98+ })
99+ }
100+}
101+
15102func TestBackwardList(t *testing.T) {
16103 t.Run("within height", func(t *testing.T) {
17104 t.Parallel()
@@ -19,7 +106,7 @@ func TestBackwardList(t *testing.T) {
19106 l.SetSize(10, 20)
20107 items := []Item{}
21108 for i := range 5 {
22− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
109+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
23110 items = append(items, item)
24111 }
25112 cmd := l.SetItems(items)
@@ -36,7 +123,7 @@ func TestBackwardList(t *testing.T) {
36123 t.Parallel()
37124 items := []Item{}
38125 for i := range 5 {
39− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
126+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
40127 items = append(items, item)
41128 }
42129 l := New(WithDirection(Backward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
@@ -54,7 +141,7 @@ func TestBackwardList(t *testing.T) {
54141 l.SetSize(10, 5)
55142 items := []Item{}
56143 for i := range 10 {
57− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
144+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
58145 items = append(items, item)
59146 }
60147 cmd := l.SetItems(items)
@@ -70,7 +157,7 @@ func TestBackwardList(t *testing.T) {
70157 l.SetSize(10, 5)
71158 items := []Item{}
72159 for i := range 10 {
73− item := NewSimpleItem(fmt.Sprintf("Item %d\nLine2", i))
160+ item := NewSelectsableItem(fmt.Sprintf("Item %d\nLine2", i))
74161 items = append(items, item)
75162 }
76163 cmd := l.SetItems(items)
@@ -86,7 +173,7 @@ func TestBackwardList(t *testing.T) {
86173 l.SetSize(10, 5)
87174 items := []Item{}
88175 for i := range 10 {
89− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
176+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
90177 items = append(items, item)
91178 }
92179 cmd := l.SetItems(items)
@@ -97,13 +184,13 @@ func TestBackwardList(t *testing.T) {
97184 l.MoveUp(1)
98185 golden.RequireEqual(t, []byte(l.View()))
99186 })
187+
100188 t.Run("should move at max to the top", func(t *testing.T) {
101− t.Parallel()
102189 l := New(WithDirection(Backward)).(*list)
103190 l.SetSize(10, 5)
104191 items := []Item{}
105192 for i := range 10 {
106− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
193+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
107194 items = append(items, item)
108195 }
109196 cmd := l.SetItems(items)
@@ -121,7 +208,7 @@ func TestBackwardList(t *testing.T) {
121208 l.SetSize(10, 5)
122209 items := []Item{}
123210 for i := range 10 {
124− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
211+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
125212 items = append(items, item)
126213 }
127214 cmd := l.SetItems(items)
@@ -138,7 +225,7 @@ func TestBackwardList(t *testing.T) {
138225 l.SetSize(10, 5)
139226 items := []Item{}
140227 for i := range 10 {
141− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
228+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
142229 items = append(items, item)
143230 }
144231 cmd := l.SetItems(items)
@@ -156,7 +243,7 @@ func TestBackwardList(t *testing.T) {
156243 l.SetSize(10, 5)
157244 items := []Item{}
158245 for i := range 10 {
159− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
246+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
160247 items = append(items, item)
161248 }
162249 cmd := l.SetItems(items)
@@ -167,15 +254,15 @@ func TestBackwardList(t *testing.T) {
167254 selectedInx := len(l.items) - 2
168255 currentItem := items[len(l.items)-1]
169256 nextItem := items[selectedInx]
170− assert.False(t, nextItem.(SimpleItem).IsFocused())
171− assert.True(t, currentItem.(SimpleItem).IsFocused())
257+ assert.False(t, nextItem.(SelectableItem).IsFocused())
258+ assert.True(t, currentItem.(SelectableItem).IsFocused())
172259 cmd = l.SelectItemAbove()
173260 if cmd != nil {
174261 cmd()
175262 }
176263
177264 assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
178− assert.True(t, l.items[selectedInx].(SimpleItem).IsFocused())
265+ assert.True(t, l.items[selectedInx].(SelectableItem).IsFocused())
179266
180267 golden.RequireEqual(t, []byte(l.View()))
181268 })
@@ -185,7 +272,7 @@ func TestBackwardList(t *testing.T) {
185272 l.SetSize(10, 5)
186273 items := []Item{}
187274 for i := range 10 {
188− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
275+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
189276 items = append(items, item)
190277 }
191278 cmd := l.SetItems(items)
@@ -210,7 +297,7 @@ func TestForwardList(t *testing.T) {
210297 l.SetSize(10, 20)
211298 items := []Item{}
212299 for i := range 5 {
213− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
300+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
214301 items = append(items, item)
215302 }
216303 cmd := l.SetItems(items)
@@ -227,7 +314,7 @@ func TestForwardList(t *testing.T) {
227314 t.Parallel()
228315 items := []Item{}
229316 for i := range 5 {
230− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
317+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
231318 items = append(items, item)
232319 }
233320 l := New(WithDirection(Forward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
@@ -245,7 +332,7 @@ func TestForwardList(t *testing.T) {
245332 l.SetSize(10, 5)
246333 items := []Item{}
247334 for i := range 10 {
248− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
335+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
249336 items = append(items, item)
250337 }
251338 cmd := l.SetItems(items)
@@ -261,7 +348,7 @@ func TestForwardList(t *testing.T) {
261348 l.SetSize(10, 5)
262349 items := []Item{}
263350 for i := range 10 {
264− item := NewSimpleItem(fmt.Sprintf("Item %d\nLine2", i))
351+ item := NewSelectsableItem(fmt.Sprintf("Item %d\nLine2", i))
265352 items = append(items, item)
266353 }
267354 cmd := l.SetItems(items)
@@ -277,7 +364,7 @@ func TestForwardList(t *testing.T) {
277364 l.SetSize(10, 5)
278365 items := []Item{}
279366 for i := range 10 {
280− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
367+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
281368 items = append(items, item)
282369 }
283370 cmd := l.SetItems(items)
@@ -288,13 +375,13 @@ func TestForwardList(t *testing.T) {
288375 l.MoveDown(1)
289376 golden.RequireEqual(t, []byte(l.View()))
290377 })
291− t.Run("should move at max to the top", func(t *testing.T) {
378+ t.Run("should move at max to the bottom", func(t *testing.T) {
292379 t.Parallel()
293380 l := New(WithDirection(Forward)).(*list)
294381 l.SetSize(10, 5)
295382 items := []Item{}
296383 for i := range 10 {
297− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
384+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
298385 items = append(items, item)
299386 }
300387 cmd := l.SetItems(items)
@@ -312,7 +399,7 @@ func TestForwardList(t *testing.T) {
312399 l.SetSize(10, 5)
313400 items := []Item{}
314401 for i := range 10 {
315− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
402+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
316403 items = append(items, item)
317404 }
318405 cmd := l.SetItems(items)
@@ -329,7 +416,7 @@ func TestForwardList(t *testing.T) {
329416 l.SetSize(10, 5)
330417 items := []Item{}
331418 for i := range 10 {
332− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
419+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
333420 items = append(items, item)
334421 }
335422 cmd := l.SetItems(items)
@@ -347,7 +434,7 @@ func TestForwardList(t *testing.T) {
347434 l.SetSize(10, 5)
348435 items := []Item{}
349436 for i := range 10 {
350− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
437+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
351438 items = append(items, item)
352439 }
353440 cmd := l.SetItems(items)
@@ -358,15 +445,15 @@ func TestForwardList(t *testing.T) {
358445 selectedInx := 1
359446 currentItem := items[0]
360447 nextItem := items[selectedInx]
361− assert.False(t, nextItem.(SimpleItem).IsFocused())
362− assert.True(t, currentItem.(SimpleItem).IsFocused())
448+ assert.False(t, nextItem.(SelectableItem).IsFocused())
449+ assert.True(t, currentItem.(SelectableItem).IsFocused())
363450 cmd = l.SelectItemBelow()
364451 if cmd != nil {
365452 cmd()
366453 }
367454
368455 assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
369− assert.True(t, l.items[selectedInx].(SimpleItem).IsFocused())
456+ assert.True(t, l.items[selectedInx].(SelectableItem).IsFocused())
370457
371458 golden.RequireEqual(t, []byte(l.View()))
372459 })
@@ -376,7 +463,7 @@ func TestForwardList(t *testing.T) {
376463 l.SetSize(10, 5)
377464 items := []Item{}
378465 for i := range 10 {
379− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
466+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
380467 items = append(items, item)
381468 }
382469 cmd := l.SetItems(items)
@@ -394,7 +481,28 @@ func TestForwardList(t *testing.T) {
394481 })
395482}
396483
397−type SimpleItem interface {
484+func TestListSelection(t *testing.T) {
485+ t.Run("should skip none selectable items initially", func(t *testing.T) {
486+ t.Parallel()
487+ l := New(WithDirection(Forward)).(*list)
488+ l.SetSize(100, 10)
489+ items := []Item{}
490+ items = append(items, NewSimpleItem("None Selectable"))
491+ for i := range 5 {
492+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
493+ items = append(items, item)
494+ }
495+ cmd := l.SetItems(items)
496+ if cmd != nil {
497+ cmd()
498+ }
499+
500+ assert.Equal(t, items[1].ID(), l.selectedItem)
501+ golden.RequireEqual(t, []byte(l.View()))
502+ })
503+}
504+
505+type SelectableItem interface {
398506 Item
399507 layout.Focusable
400508}
@@ -403,15 +511,24 @@ type simpleItem struct {
403511 width int
404512 content string
405513 id string
514+}
515+type selectableItem struct {
516+ *simpleItem
406517 focused bool
407518}
408519
409−func NewSimpleItem(content string) SimpleItem {
520+func NewSimpleItem(content string) *simpleItem {
410521 return &simpleItem{
522+ id: uuid.NewString(),
411523 width: 0,
412524 content: content,
413− focused: false,
414− id: uuid.NewString(),
525+ }
526+}
527+
528+func NewSelectsableItem(content string) SelectableItem {
529+ return &selectableItem{
530+ simpleItem: NewSimpleItem(content),
531+ focused: false,
415532 }
416533}
417534
@@ -428,9 +545,6 @@ func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
428545}
429546
430547func (s *simpleItem) View() string {
431− if s.focused {
432− return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
433− }
434548 return lipgloss.NewStyle().Width(s.width).Render(s.content)
435549}
436550
@@ -444,19 +558,26 @@ func (s *simpleItem) SetSize(width int, height int) tea.Cmd {
444558 return nil
445559}
446560
561+func (s *selectableItem) View() string {
562+ if s.focused {
563+ return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
564+ }
565+ return lipgloss.NewStyle().Width(s.width).Render(s.content)
566+}
567+
447568// Blur implements SimpleItem.
448−func (s *simpleItem) Blur() tea.Cmd {
569+func (s *selectableItem) Blur() tea.Cmd {
449570 s.focused = false
450571 return nil
451572}
452573
453574// Focus implements SimpleItem.
454−func (s *simpleItem) Focus() tea.Cmd {
575+func (s *selectableItem) Focus() tea.Cmd {
455576 s.focused = true
456577 return nil
457578}
458579
459580// IsFocused implements SimpleItem.
460−func (s *simpleItem) IsFocused() bool {
581+func (s *selectableItem) IsFocused() bool {
461582 return s.focused
462583}