diff --git a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs index a2a22aa0602..93dd2c19d2c 100644 --- a/src/Avalonia.Base/Input/Navigation/TabNavigation.cs +++ b/src/Avalonia.Base/Input/Navigation/TabNavigation.cs @@ -23,10 +23,16 @@ internal static class TabNavigation if (IsTabStop(container)) return container; - // Using ActiveElement if set - var activeElement = GetActiveElement(container); - if (activeElement != null) - return GetNextTab(null, activeElement, true); + // Using ActiveElement if set. A None group never hands out an element + // inside itself, not even a remembered one: ItemsControl records the + // last focused child as the active element, so without this an + // ItemsControl with TabNavigation="None" is still entered. + if (tabbingType != KeyboardNavigationMode.None) + { + var activeElement = GetActiveElement(container); + if (activeElement != null) + return GetNextTab(null, activeElement, true); + } } else { @@ -95,8 +101,11 @@ internal static class TabNavigation if (e == null) { - // Using ActiveElement if set - var activeElement = GetActiveElement(container); + // Using ActiveElement if set, except for None groups, which never hand + // out an element inside themselves. See the matching note in GetNextTab. + var activeElement = tabbingType == KeyboardNavigationMode.None + ? null + : GetActiveElement(container); if (activeElement != null) return GetPrevTab(null, activeElement, true); else diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs index 7e8dbde0cb0..444b86abe3f 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_Tab.cs @@ -639,6 +639,76 @@ public void Next_None_Skips_Container() Assert.Equal(next, result); } + [Fact] + public void Next_None_Skips_Container_With_Active_Element_Inside() + { + StackPanel container; + Button current; + Button inside; + Button next; + + var top = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + (current = new Button { Name = "Button1" }), + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None, + Children = + { + (inside = new Button { Name = "Button2" }), + new Button { Name = "Button3" }, + } + }), + (next = new Button { Name = "Button4" }), + } + }; + + // ItemsControl.OnGotFocus records the last focused child as the active + // element, so a ListBox whose item has been focused has one set. + KeyboardNavigation.SetTabOnceActiveElement(container, inside); + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Next); + + Assert.Equal(next, result); + } + + [Fact] + public void Previous_None_Skips_Container_With_Active_Element_Inside() + { + StackPanel container; + Button current; + Button inside; + Button previous; + + var top = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + (previous = new Button { Name = "Button1" }), + (container = new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None, + Children = + { + new Button { Name = "Button2" }, + (inside = new Button { Name = "Button3" }), + } + }), + (current = new Button { Name = "Button4" }), + } + }; + + KeyboardNavigation.SetTabOnceActiveElement(container, inside); + + var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous); + + Assert.Equal(previous, result); + } + [Fact] public void Previous_Continue_Returns_Previous_Control_In_Container() { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 2fa6a47d27c..28fd58ad6de 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -633,6 +633,45 @@ private static FuncControlTemplate ScrollViewerTemplate() }); } + [Fact] + public void TabNavigation_None_Is_Skipped_When_An_Item_Is_Selected() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Button before; + Button after; + + // Selecting an item records it as the tab-once active element, and the + // navigation code follows that without consulting the navigation mode. + var target = new ListBox + { + Template = ListBoxTemplate(), + ItemsSource = new[] { "Foo", "Bar" }, + SelectedIndex = 0, + Width = 100, + Height = 100, + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None, + }; + + var root = new TestRoot(new StackPanel + { + [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, + Children = + { + (before = new Button()), + target, + (after = new Button()), + } + }); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + var result = KeyboardNavigationHandler.GetNext(before, NavigationDirection.Next); + + Assert.Same(after, result); + } + } + private static void Prepare(ListBox target) { target.Width = target.Height = 100;