@@ -12,6 +12,93 @@ import (
12 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/assert"
15+func TestListPosition(t *testing.T) {
16+ type positionOffsetTest struct {
29+ tests := []positionOffsetTest{
32+ test: "should have correct position initially when forward",
43+ test: "should offset start and end by one when moving down by one",
54+ test: "should have correct position initially when backward",
65+ test: "should offset the start and end by one when moving up by one",
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)
80+ for i := range c.numItems {
81+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
82+ items = append(items, item)
84+ cmd := l.SetItems(items)
93+ l.MoveDown(c.moveDown)
95+ start, end := l.viewPosition()
96+ assert.Equal(t, c.expectedStart, start)
97+ assert.Equal(t, c.expectedEnd, end)
15func TestBackwardList(t *testing.T) {
102func TestBackwardList(t *testing.T) {
16 t.Run("within height", func(t *testing.T) {
103 t.Run("within height", func(t *testing.T) {
17 t.Parallel()
104 t.Parallel()
@@ -19,7 +106,7 @@ func TestBackwardList(t *testing.T) {
19 l.SetSize(10, 20)
106 l.SetSize(10, 20)
20 items := []Item{}
107 items := []Item{}
21 for i := range 5 {
108 for i := range 5 {
22− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
109+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
23 items = append(items, item)
110 items = append(items, item)
25 cmd := l.SetItems(items)
112 cmd := l.SetItems(items)
@@ -36,7 +123,7 @@ func TestBackwardList(t *testing.T) {
36 t.Parallel()
123 t.Parallel()
37 items := []Item{}
124 items := []Item{}
38 for i := range 5 {
125 for i := range 5 {
39− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
126+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
40 items = append(items, item)
127 items = append(items, item)
42 l := New(WithDirection(Backward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
129 l := New(WithDirection(Backward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
@@ -54,7 +141,7 @@ func TestBackwardList(t *testing.T) {
54 l.SetSize(10, 5)
141 l.SetSize(10, 5)
55 items := []Item{}
142 items := []Item{}
56 for i := range 10 {
143 for i := range 10 {
57− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
144+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
58 items = append(items, item)
145 items = append(items, item)
60 cmd := l.SetItems(items)
147 cmd := l.SetItems(items)
@@ -70,7 +157,7 @@ func TestBackwardList(t *testing.T) {
70 l.SetSize(10, 5)
157 l.SetSize(10, 5)
71 items := []Item{}
158 items := []Item{}
72 for i := range 10 {
159 for i := range 10 {
73− item := NewSimpleItem(fmt.Sprintf("Item %d\nLine2", i))
160+ item := NewSelectsableItem(fmt.Sprintf("Item %d\nLine2", i))
74 items = append(items, item)
161 items = append(items, item)
76 cmd := l.SetItems(items)
163 cmd := l.SetItems(items)
@@ -86,7 +173,7 @@ func TestBackwardList(t *testing.T) {
86 l.SetSize(10, 5)
173 l.SetSize(10, 5)
87 items := []Item{}
174 items := []Item{}
88 for i := range 10 {
175 for i := range 10 {
89− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
176+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
90 items = append(items, item)
177 items = append(items, item)
92 cmd := l.SetItems(items)
179 cmd := l.SetItems(items)
@@ -97,13 +184,13 @@ func TestBackwardList(t *testing.T) {
97 l.MoveUp(1)
184 l.MoveUp(1)
98 golden.RequireEqual(t, []byte(l.View()))
185 golden.RequireEqual(t, []byte(l.View()))
100 t.Run("should move at max to the top", func(t *testing.T) {
188 t.Run("should move at max to the top", func(t *testing.T) {
102 l := New(WithDirection(Backward)).(*list)
189 l := New(WithDirection(Backward)).(*list)
103 l.SetSize(10, 5)
190 l.SetSize(10, 5)
104 items := []Item{}
191 items := []Item{}
105 for i := range 10 {
192 for i := range 10 {
106− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
193+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
107 items = append(items, item)
194 items = append(items, item)
109 cmd := l.SetItems(items)
196 cmd := l.SetItems(items)
@@ -121,7 +208,7 @@ func TestBackwardList(t *testing.T) {
121 l.SetSize(10, 5)
208 l.SetSize(10, 5)
122 items := []Item{}
209 items := []Item{}
123 for i := range 10 {
210 for i := range 10 {
124− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
211+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
125 items = append(items, item)
212 items = append(items, item)
127 cmd := l.SetItems(items)
214 cmd := l.SetItems(items)
@@ -138,7 +225,7 @@ func TestBackwardList(t *testing.T) {
138 l.SetSize(10, 5)
225 l.SetSize(10, 5)
139 items := []Item{}
226 items := []Item{}
140 for i := range 10 {
227 for i := range 10 {
141− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
228+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
142 items = append(items, item)
229 items = append(items, item)
144 cmd := l.SetItems(items)
231 cmd := l.SetItems(items)
@@ -156,7 +243,7 @@ func TestBackwardList(t *testing.T) {
156 l.SetSize(10, 5)
243 l.SetSize(10, 5)
157 items := []Item{}
244 items := []Item{}
158 for i := range 10 {
245 for i := range 10 {
159− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
246+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
160 items = append(items, item)
247 items = append(items, item)
162 cmd := l.SetItems(items)
249 cmd := l.SetItems(items)
@@ -167,15 +254,15 @@ func TestBackwardList(t *testing.T) {
167 selectedInx := len(l.items) - 2
254 selectedInx := len(l.items) - 2
168 currentItem := items[len(l.items)-1]
255 currentItem := items[len(l.items)-1]
169 nextItem := items[selectedInx]
256 nextItem := items[selectedInx]
170− assert.False(t, nextItem.(SimpleItem).IsFocused())
257+ assert.False(t, nextItem.(SelectableItem).IsFocused())
171− assert.True(t, currentItem.(SimpleItem).IsFocused())
258+ assert.True(t, currentItem.(SelectableItem).IsFocused())
172 cmd = l.SelectItemAbove()
259 cmd = l.SelectItemAbove()
173 if cmd != nil {
260 if cmd != nil {
177 assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
264 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())
180 golden.RequireEqual(t, []byte(l.View()))
267 golden.RequireEqual(t, []byte(l.View()))
@@ -185,7 +272,7 @@ func TestBackwardList(t *testing.T) {
185 l.SetSize(10, 5)
272 l.SetSize(10, 5)
186 items := []Item{}
273 items := []Item{}
187 for i := range 10 {
274 for i := range 10 {
188− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
275+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
189 items = append(items, item)
276 items = append(items, item)
191 cmd := l.SetItems(items)
278 cmd := l.SetItems(items)
@@ -210,7 +297,7 @@ func TestForwardList(t *testing.T) {
210 l.SetSize(10, 20)
297 l.SetSize(10, 20)
211 items := []Item{}
298 items := []Item{}
212 for i := range 5 {
299 for i := range 5 {
213− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
300+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
214 items = append(items, item)
301 items = append(items, item)
216 cmd := l.SetItems(items)
303 cmd := l.SetItems(items)
@@ -227,7 +314,7 @@ func TestForwardList(t *testing.T) {
227 t.Parallel()
314 t.Parallel()
228 items := []Item{}
315 items := []Item{}
229 for i := range 5 {
316 for i := range 5 {
230− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
317+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
231 items = append(items, item)
318 items = append(items, item)
233 l := New(WithDirection(Forward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
320 l := New(WithDirection(Forward), WithGap(1), WithSelectedItem(items[2].ID())).(*list)
@@ -245,7 +332,7 @@ func TestForwardList(t *testing.T) {
245 l.SetSize(10, 5)
332 l.SetSize(10, 5)
246 items := []Item{}
333 items := []Item{}
247 for i := range 10 {
334 for i := range 10 {
248− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
335+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
249 items = append(items, item)
336 items = append(items, item)
251 cmd := l.SetItems(items)
338 cmd := l.SetItems(items)
@@ -261,7 +348,7 @@ func TestForwardList(t *testing.T) {
261 l.SetSize(10, 5)
348 l.SetSize(10, 5)
262 items := []Item{}
349 items := []Item{}
263 for i := range 10 {
350 for i := range 10 {
264− item := NewSimpleItem(fmt.Sprintf("Item %d\nLine2", i))
351+ item := NewSelectsableItem(fmt.Sprintf("Item %d\nLine2", i))
265 items = append(items, item)
352 items = append(items, item)
267 cmd := l.SetItems(items)
354 cmd := l.SetItems(items)
@@ -277,7 +364,7 @@ func TestForwardList(t *testing.T) {
277 l.SetSize(10, 5)
364 l.SetSize(10, 5)
278 items := []Item{}
365 items := []Item{}
279 for i := range 10 {
366 for i := range 10 {
280− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
367+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
281 items = append(items, item)
368 items = append(items, item)
283 cmd := l.SetItems(items)
370 cmd := l.SetItems(items)
@@ -288,13 +375,13 @@ func TestForwardList(t *testing.T) {
288 l.MoveDown(1)
375 l.MoveDown(1)
289 golden.RequireEqual(t, []byte(l.View()))
376 golden.RequireEqual(t, []byte(l.View()))
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) {
292 t.Parallel()
379 t.Parallel()
293 l := New(WithDirection(Forward)).(*list)
380 l := New(WithDirection(Forward)).(*list)
294 l.SetSize(10, 5)
381 l.SetSize(10, 5)
295 items := []Item{}
382 items := []Item{}
296 for i := range 10 {
383 for i := range 10 {
297− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
384+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
298 items = append(items, item)
385 items = append(items, item)
300 cmd := l.SetItems(items)
387 cmd := l.SetItems(items)
@@ -312,7 +399,7 @@ func TestForwardList(t *testing.T) {
312 l.SetSize(10, 5)
399 l.SetSize(10, 5)
313 items := []Item{}
400 items := []Item{}
314 for i := range 10 {
401 for i := range 10 {
315− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
402+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
316 items = append(items, item)
403 items = append(items, item)
318 cmd := l.SetItems(items)
405 cmd := l.SetItems(items)
@@ -329,7 +416,7 @@ func TestForwardList(t *testing.T) {
329 l.SetSize(10, 5)
416 l.SetSize(10, 5)
330 items := []Item{}
417 items := []Item{}
331 for i := range 10 {
418 for i := range 10 {
332− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
419+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
333 items = append(items, item)
420 items = append(items, item)
335 cmd := l.SetItems(items)
422 cmd := l.SetItems(items)
@@ -347,7 +434,7 @@ func TestForwardList(t *testing.T) {
347 l.SetSize(10, 5)
434 l.SetSize(10, 5)
348 items := []Item{}
435 items := []Item{}
349 for i := range 10 {
436 for i := range 10 {
350− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
437+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
351 items = append(items, item)
438 items = append(items, item)
353 cmd := l.SetItems(items)
440 cmd := l.SetItems(items)
@@ -358,15 +445,15 @@ func TestForwardList(t *testing.T) {
358 selectedInx := 1
445 selectedInx := 1
359 currentItem := items[0]
446 currentItem := items[0]
360 nextItem := items[selectedInx]
447 nextItem := items[selectedInx]
361− assert.False(t, nextItem.(SimpleItem).IsFocused())
448+ assert.False(t, nextItem.(SelectableItem).IsFocused())
362− assert.True(t, currentItem.(SimpleItem).IsFocused())
449+ assert.True(t, currentItem.(SelectableItem).IsFocused())
363 cmd = l.SelectItemBelow()
450 cmd = l.SelectItemBelow()
364 if cmd != nil {
451 if cmd != nil {
368 assert.Equal(t, l.selectedItem, l.items[selectedInx].ID())
455 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())
371 golden.RequireEqual(t, []byte(l.View()))
458 golden.RequireEqual(t, []byte(l.View()))
@@ -376,7 +463,7 @@ func TestForwardList(t *testing.T) {
376 l.SetSize(10, 5)
463 l.SetSize(10, 5)
377 items := []Item{}
464 items := []Item{}
378 for i := range 10 {
465 for i := range 10 {
379− item := NewSimpleItem(fmt.Sprintf("Item %d", i))
466+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
380 items = append(items, item)
467 items = append(items, item)
382 cmd := l.SetItems(items)
469 cmd := l.SetItems(items)
@@ -394,7 +481,28 @@ func TestForwardList(t *testing.T) {
484+func TestListSelection(t *testing.T) {
485+ t.Run("should skip none selectable items initially", func(t *testing.T) {
487+ l := New(WithDirection(Forward)).(*list)
490+ items = append(items, NewSimpleItem("None Selectable"))
492+ item := NewSelectsableItem(fmt.Sprintf("Item %d", i))
493+ items = append(items, item)
495+ cmd := l.SetItems(items)
500+ assert.Equal(t, items[1].ID(), l.selectedItem)
501+ golden.RequireEqual(t, []byte(l.View()))
397−type SimpleItem interface {
505+type SelectableItem interface {
399 layout.Focusable
507 layout.Focusable
@@ -403,15 +511,24 @@ type simpleItem struct {
403 width int
511 width int
404 content string
512 content string
405 id string
513 id string
515+type selectableItem struct {
406 focused bool
517 focused bool
409−func NewSimpleItem(content string) SimpleItem {
520+func NewSimpleItem(content string) *simpleItem {
410 return &simpleItem{
521 return &simpleItem{
522+ id: uuid.NewString(),
411 width: 0,
523 width: 0,
412 content: content,
524 content: content,
528+func NewSelectsableItem(content string) SelectableItem {
529+ return &selectableItem{
530+ simpleItem: NewSimpleItem(content),
413− focused: false,
531+ focused: false,
414− id: uuid.NewString(),
@@ -428,9 +545,6 @@ func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
430func (s *simpleItem) View() string {
547func (s *simpleItem) View() string {
432− return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
434 return lipgloss.NewStyle().Width(s.width).Render(s.content)
548 return lipgloss.NewStyle().Width(s.width).Render(s.content)
@@ -444,19 +558,26 @@ func (s *simpleItem) SetSize(width int, height int) tea.Cmd {
444 return nil
558 return nil
561+func (s *selectableItem) View() string {
563+ return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
565+ return lipgloss.NewStyle().Width(s.width).Render(s.content)
447// Blur implements SimpleItem.
568// Blur implements SimpleItem.
448−func (s *simpleItem) Blur() tea.Cmd {
569+func (s *selectableItem) Blur() tea.Cmd {
449 s.focused = false
570 s.focused = false
450 return nil
571 return nil
453// Focus implements SimpleItem.
574// Focus implements SimpleItem.
454−func (s *simpleItem) Focus() tea.Cmd {
575+func (s *selectableItem) Focus() tea.Cmd {
455 s.focused = true
576 s.focused = true
456 return nil
577 return nil
459// IsFocused implements SimpleItem.
580// IsFocused implements SimpleItem.
460−func (s *simpleItem) IsFocused() bool {
581+func (s *selectableItem) IsFocused() bool {
461 return s.focused
582 return s.focused