From d3723c63f18b1865e118a71b03fcb30497e6d7f8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 21 Jul 2022 14:16:29 -0400 Subject: [PATCH] Merge pull request #8514 from pr8x/move-first-last-skip-disabled Skip disabled controls when moving to first/last item --- .../Resources/Resource.Designer.cs | 2 +- .../Resources/Resource.Designer.cs | 2 +- src/Avalonia.Controls/ItemsControl.cs | 43 +++++++++---- .../Platform/DefaultMenuInteractionHandler.cs | 10 ++- .../Primitives/SelectingItemsControlTests.cs | 63 ++++++++++++++++--- 5 files changed, 95 insertions(+), 25 deletions(-) diff --git a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs index b1ca548e2c..da6f2420c9 100644 --- a/samples/ControlCatalog.Android/Resources/Resource.Designer.cs +++ b/samples/ControlCatalog.Android/Resources/Resource.Designer.cs @@ -14,7 +14,7 @@ namespace ControlCatalog.Android { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "13.1.0.5")] public partial class Resource { diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs index 83db67fcee..877346b2e5 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs +++ b/src/Android/Avalonia.AndroidTestApplication/Resources/Resource.Designer.cs @@ -14,7 +14,7 @@ namespace Avalonia.AndroidTestApplication { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "13.1.0.5")] public partial class Resource { diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 77894fd2b8..f0cdf616a7 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -503,29 +503,50 @@ namespace Avalonia.Controls IInputElement from, bool wrap) { - IInputElement result; - var c = from; + var current = from; - do + for (;;) { - result = container.GetControl(direction, c, wrap); - from = from ?? result; + var result = container.GetControl(direction, current, wrap); - if (result != null && - result.Focusable && + if (result is null) + { + return null; + } + + if (result.Focusable && result.IsEffectivelyEnabled && result.IsEffectivelyVisible) { return result; } - c = result; - } while (c != null && c != from && direction != NavigationDirection.First && direction != NavigationDirection.Last); + current = result; - return null; + if (current == from) + { + return null; + } + + switch (direction) + { + //We did not find an enabled first item. Move downwards until we find one. + case NavigationDirection.First: + direction = NavigationDirection.Down; + from = result; + break; + + //We did not find an enabled last item. Move upwards until we find one. + case NavigationDirection.Last: + direction = NavigationDirection.Up; + from = result; + break; + + } + } } - private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e) + private void PresenterChildIndexChanged(object? sender, ChildIndexChangedEventArgs e) { _childIndexChanged?.Invoke(this, e); } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index bca1807a38..b1305737b2 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -52,7 +52,7 @@ namespace Avalonia.Controls.Platform Menu.PointerPressed += PointerPressed; Menu.PointerReleased += PointerReleased; Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); - Menu.AddHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); + Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened); Menu.AddHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.AddHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved); @@ -88,7 +88,7 @@ namespace Avalonia.Controls.Platform Menu.PointerPressed -= PointerPressed; Menu.PointerReleased -= PointerReleased; Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed); - Menu.RemoveHandler(Avalonia.Controls.Menu.MenuOpenedEvent, this.MenuOpened); + Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened); Menu.RemoveHandler(MenuItem.PointerEnterItemEvent, PointerEnter); Menu.RemoveHandler(MenuItem.PointerLeaveItemEvent, PointerLeave); Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved); @@ -169,7 +169,11 @@ namespace Avalonia.Controls.Platform case Key.Left: { - if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + if (item is { IsSubMenuOpen: true, SelectedItem: null }) + { + item.Close(); + } + else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent) { parent.Close(); parent.Focus(); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 654f502e70..024e40000a 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1605,8 +1605,8 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(new[] { "Bar" }, selectedItems); } - [Fact] - public void MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls() + [Fact(Timeout = 2000)] + public async Task MoveSelection_Wrap_Does_Not_Hang_With_No_Focusable_Controls() { // Issue #3094. var target = new TestSelector @@ -1622,11 +1622,34 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - target.MoveSelection(NavigationDirection.Next, true); + + // Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below. + // https://github.com/xunit/xunit/issues/2222 + await Task.Run(() => target.MoveSelection(NavigationDirection.Next, true)); } - [Fact(Timeout = 2000)] - public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_First_Item() + [Fact] + public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_Last_Item() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem(), + new ListBoxItem { Focusable = false }, + } + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Last, true); + + Assert.Equal(0, target.SelectedIndex); + } + + [Fact] + public void MoveSelection_Skips_Non_Focusable_Controls_When_Moving_To_First_Item() { var target = new TestSelector { @@ -1640,22 +1663,43 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); + target.MoveSelection(NavigationDirection.Last, true); + + Assert.Equal(1, target.SelectedIndex); + } - // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below. + [Fact(Timeout = 2000)] + public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_First_Item() + { + var target = new TestSelector + { + Template = Template(), + Items = new[] + { + new ListBoxItem { Focusable = false }, + new ListBoxItem { Focusable = false }, + } + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + // Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below. // https://github.com/xunit/xunit/issues/2222 await Task.Run(() => target.MoveSelection(NavigationDirection.First, true)); + Assert.Equal(-1, target.SelectedIndex); } [Fact(Timeout = 2000)] - public async Task MoveSelection_Does_Not_Hang_With_No_Focusable_Controls_And_Moving_Selection_To_The_Last_Item() + public async Task MoveSelection_Does_Not_Hang_When_All_Items_Are_Non_Focusable_And_We_Move_To_Last_Item() { var target = new TestSelector { Template = Template(), Items = new[] { - new ListBoxItem(), + new ListBoxItem { Focusable = false }, new ListBoxItem { Focusable = false }, } }; @@ -1663,9 +1707,10 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - // Timeout in xUnit doesen't work with synchronous methods so we need to apply hack below. + // Timeout in xUnit doesn't work with synchronous methods so we need to apply hack below. // https://github.com/xunit/xunit/issues/2222 await Task.Run(() => target.MoveSelection(NavigationDirection.Last, true)); + Assert.Equal(-1, target.SelectedIndex); }