diff --git a/Terminal.Gui/Views/ListView/ListView.Movement.cs b/Terminal.Gui/Views/ListView/ListView.Movement.cs
index ada247c17d..516cf40980 100644
--- a/Terminal.Gui/Views/ListView/ListView.Movement.cs
+++ b/Terminal.Gui/Views/ListView/ListView.Movement.cs
@@ -9,6 +9,13 @@ public partial class ListView
/// clears any existing multi-selection.
///
/// if the selection was moved.
+ ///
+ ///
+ /// When the selection is already at the last item and is
+ /// , the selection wraps around to the first item.
+ /// Otherwise the method returns without changing the selection.
+ ///
+ ///
public bool MoveDown (bool extend = false)
{
if (Source is null || Source.Count == 0)
@@ -30,13 +37,18 @@ public bool MoveDown (bool extend = false)
// Can move down by one.
newItem = SelectedItem.Value + 1;
}
- else if (SelectedItem >= Viewport.Y + Viewport.Height)
+ else if (Viewport.Height > 0 && SelectedItem >= Viewport.Y + Viewport.Height)
{
// Just scroll viewport
Viewport = Viewport with { Y = Source.Count - Viewport.Height };
return true;
}
+ else if (TabStop == TabBehavior.NoStop)
+ {
+ // Wrap to top
+ newItem = 0;
+ }
else
{
// Already at bottom
@@ -189,6 +201,13 @@ public bool MovePageUp (bool extend = false)
/// clears any existing multi-selection.
///
/// if the selection was moved.
+ ///
+ ///
+ /// When the selection is already at the first item and is
+ /// , the selection wraps around to the last item.
+ /// Otherwise the method returns without changing the selection.
+ ///
+ ///
public bool MoveUp (bool extend = false)
{
if (Source is null || Source.Count == 0)
@@ -221,6 +240,11 @@ public bool MoveUp (bool extend = false)
return true;
}
+ else if (TabStop == TabBehavior.NoStop)
+ {
+ // Wrap to bottom
+ newItem = Source.Count - 1;
+ }
else
{
// Already at top
diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs
index f3c09fa7b1..b35445e1e5 100644
--- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs
@@ -1076,14 +1076,14 @@ public void ShowMarks_True_SpaceWithShift_RadioButton_MarksAndMovesDown ()
// Press Space+Shift again - cannot move down further
// In radio button mode, item should toggle: marked → unmarked
- Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+ Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift));
Assert.Equal (2, lv.SelectedItem); // Still at item 2
Assert.False (lv.Source.IsMarked (0));
Assert.False (lv.Source.IsMarked (1));
Assert.False (lv.Source.IsMarked (2)); // Toggled off
// Press key combo again - should toggle back to marked
- Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+ Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift));
Assert.Equal (2, lv.SelectedItem); // Still at item 2
Assert.False (lv.Source.IsMarked (0));
Assert.False (lv.Source.IsMarked (1));
@@ -1137,14 +1137,14 @@ public void ShowMarks_True_SpaceWithShift_SelectsThenDown_MultipleSelection ()
Assert.False (lv.Source.IsMarked (2));
// Press key combo again
- Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+ Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift));
Assert.Equal (2, lv.SelectedItem); // cannot move down any further
Assert.True (lv.Source.IsMarked (0));
Assert.True (lv.Source.IsMarked (1));
Assert.True (lv.Source.IsMarked (2)); // but can toggle marked
// Press key combo again
- Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift));
+ Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift));
Assert.Equal (2, lv.SelectedItem); // cannot move down any further
Assert.True (lv.Source.IsMarked (0));
Assert.True (lv.Source.IsMarked (1));
@@ -3129,4 +3129,80 @@ public void RowRender_Event_Receives_Correct_Row_Indices_After_Scroll ()
}
#endregion RowRender RowAttribute Override
+
+ #region Movement Cycling
+
+ // Copilot
+ [Fact]
+ public void MoveDown_AtBottom_WhenTabStopIsNoStop_WrapsToTop ()
+ {
+ ObservableCollection source = ["one", "two", "three"];
+ ListView lv = new () { Source = new ListWrapper (source), TabStop = TabBehavior.NoStop };
+ lv.SetFocus ();
+
+ // Move to last item
+ Assert.True (lv.MoveDown ()); // selects 0
+ Assert.True (lv.MoveDown ()); // selects 1
+ Assert.True (lv.MoveDown ()); // selects 2
+ Assert.Equal (2, lv.SelectedItem);
+
+ // Should wrap to top
+ Assert.True (lv.MoveDown ());
+ Assert.Equal (0, lv.SelectedItem);
+ }
+
+ // Copilot
+ [Fact]
+ public void MoveDown_AtBottom_WhenTabStopIsNotNoStop_ReturnsFalse ()
+ {
+ ObservableCollection source = ["one", "two", "three"];
+ ListView lv = new () { Source = new ListWrapper (source), TabStop = TabBehavior.TabStop };
+ lv.SetFocus ();
+
+ // Move to last item
+ Assert.True (lv.MoveDown ()); // selects 0
+ Assert.True (lv.MoveDown ()); // selects 1
+ Assert.True (lv.MoveDown ()); // selects 2
+ Assert.Equal (2, lv.SelectedItem);
+
+ // Should NOT wrap — returns false
+ Assert.False (lv.MoveDown ());
+ Assert.Equal (2, lv.SelectedItem);
+ }
+
+ // Copilot
+ [Fact]
+ public void MoveUp_AtTop_WhenTabStopIsNoStop_WrapsToBottom ()
+ {
+ ObservableCollection source = ["one", "two", "three"];
+ ListView lv = new () { Frame = new Rectangle (0, 0, 10, 20), Source = new ListWrapper (source), TabStop = TabBehavior.NoStop };
+ lv.SetFocus ();
+
+ // Select first item
+ Assert.True (lv.MoveDown ());
+ Assert.Equal (0, lv.SelectedItem);
+
+ // Should wrap to bottom
+ Assert.True (lv.MoveUp ());
+ Assert.Equal (2, lv.SelectedItem);
+ }
+
+ // Copilot
+ [Fact]
+ public void MoveUp_AtTop_WhenTabStopIsNotNoStop_ReturnsFalse ()
+ {
+ ObservableCollection source = ["one", "two", "three"];
+ ListView lv = new () { Frame = new Rectangle (0, 0, 10, 20), Source = new ListWrapper (source), TabStop = TabBehavior.TabStop };
+ lv.SetFocus ();
+
+ // Select first item
+ Assert.True (lv.MoveDown ());
+ Assert.Equal (0, lv.SelectedItem);
+
+ // Should NOT wrap — returns false
+ Assert.False (lv.MoveUp ());
+ Assert.Equal (0, lv.SelectedItem);
+ }
+
+ #endregion Movement Cycling
}