on main help sign in
chore: more tests
3e18da5Kujtim Hoxha
internal/tui/exp/list/list.go+70−12
@@ -1,6 +1,7 @@
1package list
1package list
2
2
3import (
3import (
4+ "fmt"
4 "strings"
5 "strings"
5
6
6 tea "github.com/charmbracelet/bubbletea/v2"
7 tea "github.com/charmbracelet/bubbletea/v2"
@@ -132,19 +133,20 @@ func (l *list) View() string {
132 view := l.rendered
133 view := l.rendered
133 lines := strings.Split(view, "\n")
134 lines := strings.Split(view, "\n")
134
135
135− start, end := l.viewPosition(len(lines))
136+ start, end := l.viewPosition()
136− lines = lines[start:end]
137+ lines = lines[start : end+1]
137 return strings.Join(lines, "\n")
138 return strings.Join(lines, "\n")
138}
139}
139
140
140−func (l *list) viewPosition(total int) (int, int) {
141+func (l *list) viewPosition() (int, int) {
141 start, end := 0, 0
142 start, end := 0, 0
143+ renderedLines := lipgloss.Height(l.rendered) - 1
142 if l.direction == Forward {
144 if l.direction == Forward {
143 start = max(0, l.offset)
145 start = max(0, l.offset)
144− end = min(l.offset+l.listHeight(), total)
146+ end = min(l.offset+l.listHeight()-1, renderedLines)
145 } else {
147 } else {
146− start = max(0, total-l.offset-l.listHeight())
148+ start = max(0, renderedLines-l.offset-l.listHeight()+1)
147− end = max(0, total-l.offset)
149+ end = max(0, renderedLines-l.offset)
148 }
150 }
149 return start, end
151 return start, end
150}
152}
@@ -200,20 +202,71 @@ func (l *list) decrementOffset(n int) {
200 }
202 }
201}
203}
202
204
205+// changeSelectedWhenNotVisible is called so we make sure we move to the next available selected that is visible
206+func (l *list) changeSelectedWhenNotVisible() tea.Cmd {
207+ var cmds []tea.Cmd
208+ start, end := l.viewPosition()
209+ currentPosition := 0
210+ itemWithinView := NotFound
211+ needsMove := false
212+
213+ for i, item := range l.items {
214+ rendered := l.renderedItems[i]
215+ itemStart := currentPosition
216+ // we remove 1 so that we actually have the row, e.x 1 row => height 1 => start 0, end 0
217+ itemEnd := itemStart + rendered.height - 1
218+ if itemStart >= start && itemEnd <= end {
219+ itemWithinView = i
220+ }
221+ if item.ID() == l.selectedItem {
222+ // item is completely above the viewport
223+ if itemStart < start && itemEnd < start {
224+ needsMove = true
225+ }
226+ // item is completely below the viewport
227+ if itemStart > end && itemEnd > end {
228+ needsMove = true
229+ }
230+ if needsMove {
231+ if focusable, ok := item.(layout.Focusable); ok {
232+ cmds = append(cmds, focusable.Blur())
233+ }
234+ l.renderedItems[i] = l.renderItem(item)
235+ } else {
236+ return nil
237+ }
238+ }
239+ if itemWithinView != NotFound && needsMove {
240+ newSelection := l.items[itemWithinView]
241+ l.selectedItem = newSelection.ID()
242+ if focusable, ok := newSelection.(layout.Focusable); ok {
243+ cmds = append(cmds, focusable.Focus())
244+ }
245+ l.renderedItems[itemWithinView] = l.renderItem(newSelection)
246+ break
247+ }
248+ currentPosition += rendered.height + l.gap
249+ }
250+ l.renderView()
251+ return tea.Batch(cmds...)
252+}
253+
203−func (l *list) MoveUp(n int) {
254+func (l *list) MoveUp(n int) tea.Cmd {
204 if l.direction == Forward {
255 if l.direction == Forward {
205 l.decrementOffset(n)
256 l.decrementOffset(n)
206 } else {
257 } else {
207 l.incrementOffset(n)
258 l.incrementOffset(n)
208 }
259 }
260+ return l.changeSelectedWhenNotVisible()
209}
261}
210
262
211−func (l *list) MoveDown(n int) {
263+func (l *list) MoveDown(n int) tea.Cmd {
212 if l.direction == Forward {
264 if l.direction == Forward {
213 l.incrementOffset(n)
265 l.incrementOffset(n)
214 } else {
266 } else {
215 l.decrementOffset(n)
267 l.decrementOffset(n)
216 }
268 }
269+ return l.changeSelectedWhenNotVisible()
217}
270}
218
271
219func (l *list) firstSelectableItemBefore(inx int) int {
272func (l *list) firstSelectableItemBefore(inx int) int {
@@ -239,10 +292,10 @@ func (l *list) moveToSelected() {
239 return
292 return
240 }
293 }
241 currentPosition := 0
294 currentPosition := 0
242− start, end := l.viewPosition(lipgloss.Height(l.rendered))
295+ start, end := l.viewPosition()
243 for _, item := range l.renderedItems {
296 for _, item := range l.renderedItems {
244 if item.id == l.selectedItem {
297 if item.id == l.selectedItem {
245− if start <= currentPosition && currentPosition <= end {
298+ if start <= currentPosition && (currentPosition+item.height) <= end {
246 return
299 return
247 }
300 }
248 // we need to go up
301 // we need to go up
@@ -354,7 +407,7 @@ func (l *list) renderForward() tea.Cmd {
354 currentIndex := 0
407 currentIndex := 0
355 for i, item := range l.items {
408 for i, item := range l.items {
356 currentIndex = i
409 currentIndex = i
357− if currentHeight > l.listHeight() {
410+ if currentHeight-1 > l.listHeight() {
358 break
411 break
359 }
412 }
360 rendered := l.renderItem(item)
413 rendered := l.renderItem(item)
@@ -387,6 +440,7 @@ func (l *list) renderBackward() tea.Cmd {
387 currentHeight := 0
440 currentHeight := 0
388 currentIndex := 0
441 currentIndex := 0
389 for i := len(l.items) - 1; i >= 0; i-- {
442 for i := len(l.items) - 1; i >= 0; i-- {
443+ fmt.Printf("rendering item %d\n", i)
390 currentIndex = i
444 currentIndex = i
391 if currentHeight > l.listHeight() {
445 if currentHeight > l.listHeight() {
392 break
446 break
@@ -397,12 +451,13 @@ func (l *list) renderBackward() tea.Cmd {
397 }
451 }
398 // initial render
452 // initial render
399 l.renderView()
453 l.renderView()
400− if currentIndex == len(l.items)-1 {
454+ if currentIndex == 0 {
401 l.isReady = true
455 l.isReady = true
402 return nil
456 return nil
403 }
457 }
404 return func() tea.Msg {
458 return func() tea.Msg {
405 for i := currentIndex; i >= 0; i-- {
459 for i := currentIndex; i >= 0; i-- {
460+ fmt.Printf("rendering item after %d\n", i)
406 rendered := l.renderItem(l.items[i])
461 rendered := l.renderItem(l.items[i])
407 l.renderedItems = append([]renderedItem{rendered}, l.renderedItems...)
462 l.renderedItems = append([]renderedItem{rendered}, l.renderedItems...)
408 }
463 }
@@ -451,6 +506,9 @@ func (l *list) renderItems() tea.Cmd {
451 l.selectLastItem()
506 l.selectLastItem()
452 }
507 }
453 }
508 }
509+ if l.direction == Forward {
510+ return l.renderForward()
511+ }
454 return l.renderBackward()
512 return l.renderBackward()
455}
513}
456
514
internal/tui/exp/list/list_test.go+159−38
@@ -12,6 +12,93 @@ import (
12 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/assert"
13)
13)
14
14
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+
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)
24 }
111 }
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)
41 }
128 }
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)
59 }
146 }
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)
75 }
162 }
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)
91 }
178 }
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()))
99 })
186 })
187+
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) {
101− t.Parallel()
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)
108 }
195 }
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)
126 }
213 }
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)
143 }
230 }
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)
161 }
248 }
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 {
174 cmd()
261 cmd()
175 }
262 }
176
263
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())
179
266
180 golden.RequireEqual(t, []byte(l.View()))
267 golden.RequireEqual(t, []byte(l.View()))
181 })
268 })
@@ -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)
190 }
277 }
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)
215 }
302 }
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)
232 }
319 }
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)
250 }
337 }
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)
266 }
353 }
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)
282 }
369 }
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()))
290 })
377 })
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)
299 }
386 }
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)
317 }
404 }
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)
334 }
421 }
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)
352 }
439 }
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 {
365 cmd()
452 cmd()
366 }
453 }
367
454
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())
370
457
371 golden.RequireEqual(t, []byte(l.View()))
458 golden.RequireEqual(t, []byte(l.View()))
372 })
459 })
@@ -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)
381 }
468 }
382 cmd := l.SetItems(items)
469 cmd := l.SetItems(items)
@@ -394,7 +481,28 @@ func TestForwardList(t *testing.T) {
394 })
481 })
395}
482}
396
483
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+
397−type SimpleItem interface {
505+type SelectableItem interface {
398 Item
506 Item
399 layout.Focusable
507 layout.Focusable
400}
508}
@@ -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
514+}
515+type selectableItem struct {
516+ *simpleItem
406 focused bool
517 focused bool
407}
518}
408
519
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,
525+ }
526+}
527+
528+func NewSelectsableItem(content string) SelectableItem {
529+ return &selectableItem{
530+ simpleItem: NewSimpleItem(content),
413− focused: false,
531+ focused: false,
414− id: uuid.NewString(),
415 }
532 }
416}
533}
417
534
@@ -428,9 +545,6 @@ func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
428}
545}
429
546
430func (s *simpleItem) View() string {
547func (s *simpleItem) View() string {
431− if s.focused {
432− return lipgloss.NewStyle().BorderLeft(true).BorderStyle(lipgloss.NormalBorder()).Width(s.width).Render(s.content)
433− }
434 return lipgloss.NewStyle().Width(s.width).Render(s.content)
548 return lipgloss.NewStyle().Width(s.width).Render(s.content)
435}
549}
436
550
@@ -444,19 +558,26 @@ func (s *simpleItem) SetSize(width int, height int) tea.Cmd {
444 return nil
558 return nil
445}
559}
446
560
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+
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
451}
572}
452
573
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
457}
578}
458
579
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
462}
583}
internal/tui/exp/list/testdata/TestBackwardList/should_move_at_max_to_the_top.golden+1−1
@@ -2,4 +2,4 @@ Item 0
2Item 1
2Item 1
3Item 2
3Item 2
4Item 3
4Item 3
5−Item 4
5+│Item 4
internal/tui/exp/list/testdata/TestBackwardList/should_move_up.golden+1−1
@@ -2,4 +2,4 @@ Item 4
2Item 5
2Item 5
3Item 6
3Item 6
4Item 7
4Item 7
5−Item 8
5+│Item 8
internal/tui/exp/list/testdata/TestBackwardList/within_height.golden−2
@@ -1,7 +1,5 @@
1Item 0
1Item 0
2
2
3−Item 0
4−
5Item 1
3Item 1
6
4
7Item 2
5Item 2
internal/tui/exp/list/testdata/TestForwardList/should_move_at_max_to_the_top.golden → internal/tui/exp/list/testdata/TestForwardList/should_move_at_max_to_the_bottom.goldenrenamed+1−1
@@ -1,4 +1,4 @@
1−Item 5
1+│Item 5
2Item 6
2Item 6
3Item 7
3Item 7
4Item 8
4Item 8
internal/tui/exp/list/testdata/TestForwardList/should_move_down.golden+1−1
@@ -1,4 +1,4 @@
1−Item 1
1+│Item 1
2Item 2
2Item 2
3Item 3
3Item 3
4Item 4
4Item 4
internal/tui/exp/list/testdata/TestForwardList/within_height.golden−2
@@ -1,7 +1,5 @@
1│Item 0
1│Item 0
2
2
3−│Item 0
4−
5Item 1
3Item 1
6
4
7Item 2
5Item 2
internal/tui/exp/list/testdata/TestListSelection/should_skip_none_selectable_items_initially.goldennew file+6
@@ -0,0 +1,6 @@
1+None Selectable
2+│Item 0
3+Item 1
4+Item 2
5+Item 3
6+Item 4