Skip to content

Commit c814741

Browse files
Copilottig
andauthored
Fix AdvanceFocus not cycling properly through peers with nested subviews (#4267)
* Initial plan * Implement fix for AdvanceFocus nested subview navigation Co-authored-by: tig <585482+tig@users.noreply.github.com> * Refactor ShouldBubbleUpForWrapping to use iteration instead of recursion Co-authored-by: tig <585482+tig@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Tig <tig@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com>
1 parent 168d233 commit c814741

File tree

2 files changed

+84
-1
lines changed

2 files changed

+84
-1
lines changed

Terminal.Gui/ViewBase/View.Navigation.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior)
9292
if (SuperView is { })
9393
{
9494
// If we are TabStop, and we have at least one other focusable peer, move to the SuperView's chain
95-
if (TabStop == TabBehavior.TabStop && SuperView is { } && SuperView.GetFocusChain (direction, behavior).Length > 1)
95+
if (TabStop == TabBehavior.TabStop && SuperView is { } && ShouldBubbleUpForWrapping (SuperView, direction, behavior))
9696
{
9797
return false;
9898
}
@@ -171,6 +171,45 @@ bool AdvanceFocusChain ()
171171
}
172172
}
173173

174+
/// <summary>
175+
/// Determines if focus should bubble up to a SuperView when wrapping would occur.
176+
/// Iteratively checks up the SuperView hierarchy to see if there are any focusable peers at any level.
177+
/// </summary>
178+
/// <param name="view">The SuperView to check.</param>
179+
/// <param name="direction">The navigation direction.</param>
180+
/// <param name="behavior">The tab behavior to filter by.</param>
181+
/// <returns>
182+
/// <see langword="true"/> if there are focusable peers at this level or any ancestor level,
183+
/// <see langword="false"/> otherwise.
184+
/// </returns>
185+
private bool ShouldBubbleUpForWrapping (View? view, NavigationDirection direction, TabBehavior? behavior)
186+
{
187+
View? currentView = view;
188+
189+
while (currentView is { })
190+
{
191+
// If this parent has multiple focusable children, we should bubble up
192+
View [] chain = currentView.GetFocusChain (direction, behavior);
193+
194+
if (chain.Length > 1)
195+
{
196+
return true;
197+
}
198+
199+
// If parent has only 1 child but parent is also TabStop with a SuperView, continue checking up the hierarchy
200+
if (currentView.TabStop == TabBehavior.TabStop && currentView.SuperView is { })
201+
{
202+
currentView = currentView.SuperView;
203+
}
204+
else
205+
{
206+
break;
207+
}
208+
}
209+
210+
return false;
211+
}
212+
174213
private bool RaiseAdvancingFocus (NavigationDirection direction, TabBehavior? behavior)
175214
{
176215
// Call the virtual method

Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,4 +644,48 @@ public void TabStop_And_CanFocus_Are_Decoupled (bool canFocus, TabBehavior tabSt
644644
Assert.Equal (canFocus, view.CanFocus);
645645
Assert.Equal (tabStop, view.TabStop);
646646
}
647+
648+
[Fact]
649+
public void AdvanceFocus_Cycles_Through_Peers_And_All_Nested_SubViews_When_Multiple ()
650+
{
651+
var top = new View { Id = "top", CanFocus = true };
652+
653+
View peer1 = new View
654+
{
655+
CanFocus = true,
656+
Id = "peer1",
657+
};
658+
659+
var peer2 = new View
660+
{
661+
CanFocus = true,
662+
Id = "peer2",
663+
};
664+
var peer2SubView = new View
665+
{
666+
Id = "peer2SubView", CanFocus = true
667+
};
668+
var v1 = new View { Id = "v1", CanFocus = true };
669+
var v2 = new View { Id = "v2", CanFocus = true };
670+
peer2SubView.Add (v1, v2);
671+
672+
peer2.Add (peer2SubView);
673+
674+
top.Add (peer1, peer2);
675+
top.SetFocus ();
676+
677+
Assert.Equal (peer1, top.MostFocused);
678+
679+
top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
680+
Assert.Equal (v1, top.MostFocused);
681+
682+
top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
683+
Assert.Equal (v2, top.MostFocused);
684+
685+
// This should cycle to peer1 - previously it incorrectly cycled to v1
686+
top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
687+
Assert.Equal (peer1, top.MostFocused);
688+
689+
top.Dispose ();
690+
}
647691
}

0 commit comments

Comments
 (0)