diff --git a/Terminal.Gui/ViewBase/View.Navigation.cs b/Terminal.Gui/ViewBase/View.Navigation.cs index 27a69ce75..7f6a66177 100644 --- a/Terminal.Gui/ViewBase/View.Navigation.cs +++ b/Terminal.Gui/ViewBase/View.Navigation.cs @@ -92,7 +92,7 @@ public partial class View // Focus and cross-view navigation management (TabStop if (SuperView is { }) { // If we are TabStop, and we have at least one other focusable peer, move to the SuperView's chain - if (TabStop == TabBehavior.TabStop && SuperView is { } && SuperView.GetFocusChain (direction, behavior).Length > 1) + if (TabStop == TabBehavior.TabStop && SuperView is { } && ShouldBubbleUpForWrapping (SuperView, direction, behavior)) { return false; } @@ -171,6 +171,45 @@ public partial class View // Focus and cross-view navigation management (TabStop } } + /// + /// Determines if focus should bubble up to a SuperView when wrapping would occur. + /// Iteratively checks up the SuperView hierarchy to see if there are any focusable peers at any level. + /// + /// The SuperView to check. + /// The navigation direction. + /// The tab behavior to filter by. + /// + /// if there are focusable peers at this level or any ancestor level, + /// otherwise. + /// + private bool ShouldBubbleUpForWrapping (View? view, NavigationDirection direction, TabBehavior? behavior) + { + View? currentView = view; + + while (currentView is { }) + { + // If this parent has multiple focusable children, we should bubble up + View [] chain = currentView.GetFocusChain (direction, behavior); + + if (chain.Length > 1) + { + return true; + } + + // If parent has only 1 child but parent is also TabStop with a SuperView, continue checking up the hierarchy + if (currentView.TabStop == TabBehavior.TabStop && currentView.SuperView is { }) + { + currentView = currentView.SuperView; + } + else + { + break; + } + } + + return false; + } + private bool RaiseAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) { // Call the virtual method diff --git a/Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs b/Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs index 814ecaa38..83548e963 100644 --- a/Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs +++ b/Tests/UnitTestsParallelizable/View/Navigation/AdvanceFocusTests.cs @@ -644,4 +644,48 @@ public class AdvanceFocusTests () Assert.Equal (canFocus, view.CanFocus); Assert.Equal (tabStop, view.TabStop); } + + [Fact] + public void AdvanceFocus_Cycles_Through_Peers_And_All_Nested_SubViews_When_Multiple () + { + var top = new View { Id = "top", CanFocus = true }; + + View peer1 = new View + { + CanFocus = true, + Id = "peer1", + }; + + var peer2 = new View + { + CanFocus = true, + Id = "peer2", + }; + var peer2SubView = new View + { + Id = "peer2SubView", CanFocus = true + }; + var v1 = new View { Id = "v1", CanFocus = true }; + var v2 = new View { Id = "v2", CanFocus = true }; + peer2SubView.Add (v1, v2); + + peer2.Add (peer2SubView); + + top.Add (peer1, peer2); + top.SetFocus (); + + Assert.Equal (peer1, top.MostFocused); + + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.Equal (v1, top.MostFocused); + + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.Equal (v2, top.MostFocused); + + // This should cycle to peer1 - previously it incorrectly cycled to v1 + top.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + Assert.Equal (peer1, top.MostFocused); + + top.Dispose (); + } }