fix(multiselect): enforce selection limit while filtering
The toggle handler's limit guard indexed the unfiltered options slice with the cursor, but the cursor tracks the filtered slice. When a filter was applied the two indices diverged, so the guard inspected the wrong option's selected state and allowed toggling past the configured Limit. Index the option that is actually being toggled instead, and add a regression test that selects up to the limit, filters, then verifies a further toggle is a no-op.
This commit is contained in:
1 parent
bb7e934525
commit
ba0822d4e9
2 files changed
+42
-1
No files matched your search
@@ -423,7 +423,7 @@ func (m *MultiSelect[T]) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
case key.Matches(msg, m.keymap.Toggle) && !m.filtering:
|
||||
for i, option := range m.options.val {
|
||||
if option.Key == m.filteredOptions[m.cursor].Key {
|
||||
if !m.options.val[m.cursor].selected && m.limit > 0 && m.numSelected() >= m.limit {
|
||||
if !m.options.val[i].selected && m.limit > 0 && m.numSelected() >= m.limit {
|
||||
break
|
||||
}
|
||||
selected := m.options.val[i].selected
|
||||
|
||||
+41
@@ -741,6 +741,47 @@ func TestMultiSelectFiltering(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultiSelectLimitEnforcedWhileFiltered(t *testing.T) {
|
||||
// Regression test: the toggle limit guard must inspect the option that is
|
||||
// actually being toggled, not the option sharing the cursor index in the
|
||||
// unfiltered list. With a filter applied the cursor indexes the filtered
|
||||
// slice, so the two diverge and the limit could be exceeded.
|
||||
field := NewMultiSelect[string]().
|
||||
Options(NewOptions("Red", "Green", "Blue")...).
|
||||
Title("Colors").
|
||||
Limit(1)
|
||||
f := NewForm(NewGroup(field))
|
||||
f.Update(f.Init())
|
||||
|
||||
// Select "Red" (the first option), reaching the limit of one.
|
||||
m := batchUpdate(f.Update(keypress('x')))
|
||||
|
||||
// Filter down to "Blue", which lives at index 2 in the unfiltered list but
|
||||
// at index 0 in the filtered list.
|
||||
m = batchUpdate(m.Update(keypress('/')))
|
||||
for _, r := range "Blue" {
|
||||
m = batchUpdate(m.Update(keypress(r)))
|
||||
}
|
||||
|
||||
// Apply the filter: filtering stops but the filtered list remains in place.
|
||||
m = batchUpdate(m.Update(codeKeypress(tea.KeyEscape)))
|
||||
|
||||
// Attempt to toggle "Blue". The limit is already reached, so this must be a
|
||||
// no-op rather than selecting a second option.
|
||||
batchUpdate(m.Update(keypress('x')))
|
||||
|
||||
value, ok := field.GetValue().([]string)
|
||||
if !ok {
|
||||
t.Fatalf("expected []string value, got %T", field.GetValue())
|
||||
}
|
||||
if len(value) != 1 {
|
||||
t.Fatalf("limit of 1 exceeded while filtering: got %d selected options %v", len(value), value)
|
||||
}
|
||||
if value[0] != "Red" {
|
||||
t.Errorf("expected the only selected option to be Red, got %q", value[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectFilteringShowsMatchesAboveTheCursor(t *testing.T) {
|
||||
// Bar and Baz sit at opposite ends of the list, so filtering for "ba"
|
||||
// while the cursor is at the bottom leaves Bar above the visible window.
|
||||
|
||||
Reference in new issue
Block a user