Repository: Evertras/bubble-table
Pull request #37 (2022-03-04 13:07:01)

Add first/last page keypresses

Evertras: Closes #30 

Patch:
diff --git a/table/keys.go b/table/keys.go
index ccbee58..3aa9fab 100644
--- a/table/keys.go
+++ b/table/keys.go
@@ -9,8 +9,10 @@ type KeyMap struct {
 
 	RowSelectToggle key.Binding
 
-	PageDown key.Binding
-	PageUp   key.Binding
+	PageDown  key.Binding
+	PageUp    key.Binding
+	PageFirst key.Binding
+	PageLast  key.Binding
 
 	Filter key.Binding
 }
@@ -33,6 +35,12 @@ func DefaultKeyMap() KeyMap {
 		PageUp: key.NewBinding(
 			key.WithKeys("left", "h", "pgup"),
 		),
+		PageFirst: key.NewBinding(
+			key.WithKeys("home", "g"),
+		),
+		PageLast: key.NewBinding(
+			key.WithKeys("end", "G"),
+		),
 		Filter: key.NewBinding(
 			key.WithKeys("/"),
 		),
diff --git a/table/pagination.go b/table/pagination.go
index f6418be..dd50038 100644
--- a/table/pagination.go
+++ b/table/pagination.go
@@ -81,6 +81,24 @@ func (m *Model) pageUp() {
 	m.rowCursorIndex = m.currentPage * m.pageSize
 }
 
+func (m *Model) pageFirst() {
+	if m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {
+		return
+	}
+
+	m.currentPage = 0
+	m.rowCursorIndex = 0
+}
+
+func (m *Model) pageLast() {
+	if m.pageSize == 0 || len(m.GetVisibleRows()) <= m.pageSize {
+		return
+	}
+
+	m.currentPage = m.MaxPages() - 1
+	m.rowCursorIndex = m.currentPage * m.pageSize
+}
+
 func (m *Model) expectedPageForRowIndex(rowIndex int) int {
 	if m.pageSize == 0 {
 		return 0
diff --git a/table/update.go b/table/update.go
index 674a9a4..23e71ef 100644
--- a/table/update.go
+++ b/table/update.go
@@ -59,6 +59,34 @@ func (m Model) updateFilterTextInput(msg tea.Msg) (Model, tea.Cmd) {
 	return m, cmd
 }
 
+func (m *Model) handleKeypress(msg tea.KeyMsg) {
+	switch {
+	case key.Matches(msg, m.keyMap.RowDown):
+		m.moveHighlightDown()
+
+	case key.Matches(msg, m.keyMap.RowUp):
+		m.moveHighlightUp()
+
+	case key.Matches(msg, m.keyMap.RowSelectToggle):
+		m.toggleSelect()
+
+	case key.Matches(msg, m.keyMap.PageDown):
+		m.pageDown()
+
+	case key.Matches(msg, m.keyMap.PageUp):
+		m.pageUp()
+
+	case key.Matches(msg, m.keyMap.PageFirst):
+		m.pageFirst()
+
+	case key.Matches(msg, m.keyMap.PageLast):
+		m.pageLast()
+
+	case key.Matches(msg, m.keyMap.Filter):
+		m.filterTextInput.Focus()
+	}
+}
+
 // Update responds to input from the user or other messages from Bubble Tea.
 func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 	if !m.focused {
@@ -71,25 +99,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
 
 	switch msg := msg.(type) {
 	case tea.KeyMsg:
-		switch {
-		case key.Matches(msg, m.keyMap.RowDown):
-			m.moveHighlightDown()
-
-		case key.Matches(msg, m.keyMap.RowUp):
-			m.moveHighlightUp()
-
-		case key.Matches(msg, m.keyMap.RowSelectToggle):
-			m.toggleSelect()
-
-		case key.Matches(msg, m.keyMap.PageDown):
-			m.pageDown()
-
-		case key.Matches(msg, m.keyMap.PageUp):
-			m.pageUp()
-
-		case key.Matches(msg, m.keyMap.Filter):
-			m.filterTextInput.Focus()
-		}
+		m.handleKeypress(msg)
 	}
 
 	return m, nil


Test patch:
diff --git a/table/update_test.go b/table/update_test.go
index 133d976..7937910 100644
--- a/table/update_test.go
+++ b/table/update_test.go
@@ -33,9 +33,56 @@ func TestUnfocusedDoesntMove(t *testing.T) {
 	assert.True(t, ok, "Failed to convert to string")
 
 	assert.Equal(t, "first", id, "Should still be on first row")
+
+	model, _ = model.Update(tea.KeyMsg{
+		Type: tea.KeyHome,
+	})
+}
+
+func TestPageKeysDoNothingWhenNoPages(t *testing.T) {
+	cols := []Column{
+		NewColumn("id", "ID", 3),
+	}
+
+	model := New(cols).WithRows([]Row{
+		NewRow(RowData{
+			"id": "first",
+		}),
+		NewRow(RowData{
+			"id": "second",
+		}),
+		NewRow(RowData{
+			"id": "third",
+		}),
+	}).Focused(true)
+
+	pageMoveKeys := []tea.Msg{
+		tea.KeyMsg{Type: tea.KeyLeft},
+		tea.KeyMsg{Type: tea.KeyRight},
+		tea.KeyMsg{Type: tea.KeyHome},
+		tea.KeyMsg{Type: tea.KeyEnd},
+	}
+
+	checkNoMove := func() string {
+		str, ok := model.HighlightedRow().Data["id"].(string)
+
+		assert.True(t, ok, "Failed to convert to string")
+
+		assert.Equal(t, "first", str, "Shouldn't move")
+
+		return str
+	}
+
+	for _, msg := range pageMoveKeys {
+		model, _ = model.Update(msg)
+		checkNoMove()
+	}
 }
 
-func TestFocusedMovesWhenArrowsPressed(t *testing.T) {
+// This is a long test with a lot of movement keys pressed, that's okay because
+// it's simply repetitive and tracking the same kind of state change many times
+// nolint: funlen
+func TestFocusedMovesWhenMoveKeysPressedPaged(t *testing.T) {
 	cols := []Column{
 		NewColumn("id", "ID", 3),
 	}
@@ -57,6 +104,8 @@ func TestFocusedMovesWhenArrowsPressed(t *testing.T) {
 	keyDown := tea.KeyMsg{Type: tea.KeyDown}
 	keyLeft := tea.KeyMsg{Type: tea.KeyLeft}
 	keyRight := tea.KeyMsg{Type: tea.KeyRight}
+	keyHome := tea.KeyMsg{Type: tea.KeyHome}
+	keyEnd := tea.KeyMsg{Type: tea.KeyEnd}
 
 	curID := func() string {
 		str, ok := model.HighlightedRow().Data["id"].(string)
@@ -91,6 +140,21 @@ func TestFocusedMovesWhenArrowsPressed(t *testing.T) {
 
 	model, _ = model.Update(keyLeft)
 	assert.Equal(t, "first", curID(), "Moving left should move back to first page")
+
+	model, _ = model.Update(keyDown)
+	assert.Equal(t, "second", curID(), "Should be back down to second row")
+
+	model, _ = model.Update(keyHome)
+	assert.Equal(t, "first", curID(), "Hitting home should go to first page and select first row")
+
+	model, _ = model.Update(keyHome)
+	assert.Equal(t, "first", curID(), "Hitting home a second time should not move pages")
+
+	model, _ = model.Update(keyEnd)
+	assert.Equal(t, "third", curID(), "Hitting end should move to last page")
+
+	model, _ = model.Update(keyEnd)
+	assert.Equal(t, "third", curID(), "Hitting end a second time should not move pages")
 }
 
 func TestFocusedMovesWithCustomKeyMap(t *testing.T) {