Repository: Evertras/bubble-table
Pull request #111 (2022-06-23 13:40:30)

Row select toggle event
Adds a user event for a row selection being toggled.


Patch:
diff --git a/examples/events/main.go b/examples/events/main.go
index 5ce0cd0..74bcbf6 100644
--- a/examples/events/main.go
+++ b/examples/events/main.go
@@ -42,26 +42,6 @@ var (
 		elementWater:    "#44f",
 		elementPlant:    "#8b8",
 	}
-
-	customBorder = table.Border{
-		Top:    "─",
-		Left:   "│",
-		Right:  "│",
-		Bottom: "─",
-
-		TopRight:    "╮",
-		TopLeft:     "╭",
-		BottomRight: "╯",
-		BottomLeft:  "╰",
-
-		TopJunction:    "┬",
-		LeftJunction:   "├",
-		RightJunction:  "┤",
-		BottomJunction: "┴",
-		InnerJunction:  "┼",
-
-		InnerDivider: "│",
-	}
 )
 
 type Pokemon struct {
@@ -102,6 +82,8 @@ type Model struct {
 	pokeTable table.Model
 
 	currentPokemonData Pokemon
+
+	lastSelectedEvent table.UserEventRowSelectToggled
 }
 
 func NewModel() Model {
@@ -126,10 +108,11 @@ func NewModel() Model {
 			table.NewColumn(columnKeyName, "Name", 13),
 			table.NewColumn(columnKeyElement, "Element", 10),
 		}).WithRows(rows).
-			Border(customBorder).
+			BorderRounded().
 			WithBaseStyle(styleBase).
 			WithPageSize(4).
-			Focused(true),
+			Focused(true).
+			SelectableRows(true),
 		currentPokemonData: pokemon[0],
 	}
 }
@@ -148,7 +131,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	cmds = append(cmds, cmd)
 
 	for _, e := range m.pokeTable.GetLastUpdateUserEvents() {
-		switch e.(type) {
+		switch e := e.(type) {
 		case table.UserEventHighlightedIndexChanged:
 			// We can pretend this is an async data retrieval, but really we already
 			// have the data, so just return it after some fake delay.  Also note
@@ -161,6 +144,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 				return selectedPokemon
 			})
+
+		case table.UserEventRowSelectToggled:
+			m.lastSelectedEvent = e
 		}
 	}
 
@@ -182,7 +168,8 @@ func (m Model) View() string {
 	view := lipgloss.JoinVertical(
 		lipgloss.Left,
 		styleSubtle.Render("Press q or ctrl+c to quit"),
-		fmt.Sprintf("%s (%s)", m.currentPokemonData.Name, m.currentPokemonData.Element),
+		fmt.Sprintf("Highlighted (200 ms delay): %s (%s)", m.currentPokemonData.Name, m.currentPokemonData.Element),
+		fmt.Sprintf("Last selected event: %d (%v)", m.lastSelectedEvent.RowIndex, m.lastSelectedEvent.IsSelected),
 		lipgloss.NewStyle().Foreground(lipgloss.Color("#8c8")).
 			Render(":D %"+fmt.Sprintf("%.1f", m.currentPokemonData.PositiveSentimentPercent)),
 		lipgloss.NewStyle().Foreground(lipgloss.Color("#c88")).
diff --git a/table/events.go b/table/events.go
index 3eaefdb..1a3d823 100644
--- a/table/events.go
+++ b/table/events.go
@@ -40,3 +40,11 @@ type UserEventHighlightedIndexChanged struct {
 	// SelectedRow is the row index that is now selected
 	SelectedRowIndex int
 }
+
+// UserEventRowSelectToggled indicates that the user has either selected or
+// deselected a row by toggling the selection.  The event contains information
+// about which row index was selected and whether it was selected or deselected.
+type UserEventRowSelectToggled struct {
+	RowIndex   int
+	IsSelected bool
+}
diff --git a/table/update.go b/table/update.go
index 8a36177..071cfe6 100644
--- a/table/update.go
+++ b/table/update.go
@@ -33,9 +33,16 @@ func (m *Model) toggleSelect() {
 	rows := make([]Row, len(m.GetVisibleRows()))
 	copy(rows, m.GetVisibleRows())
 
-	rows[m.rowCursorIndex].selected = !rows[m.rowCursorIndex].selected
+	currentSelectedState := rows[m.rowCursorIndex].selected
+
+	rows[m.rowCursorIndex].selected = !currentSelectedState
 
 	m.rows = rows
+
+	m.appendUserEvent(UserEventRowSelectToggled{
+		RowIndex:   m.rowCursorIndex,
+		IsSelected: !currentSelectedState,
+	})
 }
 
 func (m Model) updateFilterTextInput(msg tea.Msg) (Model, tea.Cmd) {


Test patch:
diff --git a/table/events_test.go b/table/events_test.go
index f2a33dc..389c031 100644
--- a/table/events_test.go
+++ b/table/events_test.go
@@ -97,3 +97,78 @@ func TestUserEventHighlightedIndexChanged(t *testing.T) {
 	events = model.GetLastUpdateUserEvents()
 	assert.Len(t, events, 0, "There's no row to change to for an empty table, event shouldn't exist")
 }
+
+// nolint: funlen // This is a bunch of checks in a row, this is fine
+func TestUserEventRowSelectToggled(t *testing.T) {
+	// Don't need any actual row data for this
+	empty := RowData{}
+
+	model := New([]Column{}).
+		Focused(true).
+		WithRows(
+			[]Row{
+				NewRow(empty),
+				NewRow(empty),
+				NewRow(empty),
+				NewRow(empty),
+			},
+		).
+		SelectableRows(true)
+
+	hitDown := func() {
+		model, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
+	}
+
+	hitSelectToggle := func() {
+		model, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
+	}
+
+	checkEvent := func(events []UserEvent, expectedRowIndex int, expectedSelectionState bool) {
+		if len(events) != 1 {
+			assert.FailNow(t, "Asked to check events with len of not 1, test is bad")
+		}
+
+		switch event := events[0].(type) {
+		case UserEventRowSelectToggled:
+			assert.Equal(t, expectedRowIndex, event.RowIndex, "Row index wrong")
+			assert.Equal(t, expectedSelectionState, event.IsSelected, "Selection state wrong")
+
+		default:
+			assert.Failf(t, "Event is not expected type UserEventRowSelectToggled", "%+v", event)
+		}
+	}
+
+	events := model.GetLastUpdateUserEvents()
+	assert.Len(t, events, 0, "Should be empty when nothing has happened")
+
+	// Try initial selection
+	hitSelectToggle()
+	events = model.GetLastUpdateUserEvents()
+	assert.Len(t, events, 1, "Missing event for selection toggle")
+	checkEvent(events, 0, true)
+
+	// Do some no-op
+	model, _ = model.Update(nil)
+	events = model.GetLastUpdateUserEvents()
+	assert.Len(t, events, 0, "Events not cleared between Updates")
+
+	// Check deselection
+	hitSelectToggle()
+	events = model.GetLastUpdateUserEvents()
+	assert.Len(t, events, 1, "Missing event to toggle select for second time")
+	checkEvent(events, 0, false)
+
+	// Try one row down... note that the row change event should clear after the
+	// first keypress
+	hitDown()
+	hitSelectToggle()
+	events = model.GetLastUpdateUserEvents()
+	assert.Len(t, events, 1, "Missing event after scrolling down")
+	checkEvent(events, 1, true)
+
+	// Check edge case of empty table
+	model = model.WithRows([]Row{})
+	hitSelectToggle()
+	events = model.GetLastUpdateUserEvents()
+	assert.Len(t, events, 0, "There's no row to select for an empty table, event shouldn't exist")
+}