diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index fe1487a7f2..19ac68b15b 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -82,12 +82,16 @@ - + - None + + None + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index b9e631a312..9a612aa94d 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -62,6 +62,8 @@ namespace IntegrationTestApp this.FindControl("BasicComboBox").SelectedIndex = 0; if (source?.Name == "ListBoxSelectionClear") this.FindControl("BasicListBox").SelectedIndex = -1; + if (source?.Name == "MenuClickedMenuItemReset") + this.FindControl("ClickedMenuItem").Text = "None"; } } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index c03c6000d8..955af8888b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -644,7 +644,9 @@ namespace Avalonia.Controls /// The property change event. private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e) { - if ((bool)e.NewValue!) + var parentMenu = Parent as Menu; + + if ((bool)e.NewValue! && (parentMenu is null || parentMenu.IsOpen)) { Focus(); } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 23ecdb2e7b..6e9ac537f1 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -149,13 +149,18 @@ namespace Avalonia.Controls.Platform case Key.Up: case Key.Down: { - if (item?.IsTopLevel == true) + if (item?.IsTopLevel == true && item.HasSubMenu) { - if (item.HasSubMenu && !item.IsSubMenuOpen) + if (!item.IsSubMenuOpen) { Open(item, true); - e.Handled = true; } + else + { + item.MoveSelection(NavigationDirection.First, true); + } + + e.Handled = true; } else { @@ -247,7 +252,8 @@ namespace Avalonia.Controls.Platform // new menu. if (item.IsSubMenuOpen && item.Parent is IMenu && - item.Parent.SelectedItem is object) + item.Parent.SelectedItem is object && + item.Parent.SelectedItem != item) { item.Close(); Open(item.Parent.SelectedItem, true); diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 9eedd17716..d22a67f389 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -53,6 +53,19 @@ namespace Avalonia.Controls.UnitTests.Platform Assert.True(e.Handled); } + [Fact] + public void Down_Selects_First_Item_Of_Already_Opened_Submenu() + { + var target = new DefaultMenuInteractionHandler(false); + var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.IsSubMenuOpen); + var e = new KeyEventArgs { Key = Key.Down, Source = item }; + + target.KeyDown(item, e); + + Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true)); + Assert.True(e.Handled); + } + [Fact] public void Right_Selects_Next_MenuItem() { diff --git a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs index 15e22f4424..3eb8646835 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ElementExtensions.cs @@ -29,6 +29,20 @@ namespace Avalonia.IntegrationTests.Appium _ => throw new ArgumentOutOfRangeException($"Unexpected IsChecked value.") }; + public static bool GetIsFocused(this AppiumWebElement element) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var active = element.WrappedDriver.SwitchTo().ActiveElement() as AppiumWebElement; + return element.Id == active?.Id; + } + else + { + // https://stackoverflow.com/questions/71807788/check-if-element-is-focused-in-appium + throw new NotSupportedException("Couldn't work out how to check if an element is focused on mac."); + } + } + public static void SendClick(this AppiumWebElement element) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -44,6 +58,11 @@ namespace Avalonia.IntegrationTests.Appium } } + public static void MovePointerOver(this AppiumWebElement element) + { + new Actions(element.WrappedDriver).MoveToElement(element).Perform(); + } + public static string GetAttribute(AppiumWebElement element, string windows, string macOS) { return element.GetAttribute(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? windows : macOS); diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index e9a433b975..98fb335061 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -1,4 +1,7 @@ -using OpenQA.Selenium.Appium; +using System.Threading; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; using Xunit; namespace Avalonia.IntegrationTests.Appium @@ -15,6 +18,12 @@ namespace Avalonia.IntegrationTests.Appium var tabs = _session.FindElementByAccessibilityId("MainTabs"); var tab = tabs.FindElementByName("Menu"); tab.Click(); + + var reset = _session.FindElementByAccessibilityId("MenuClickedMenuItemReset"); + reset.Click(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("None", clickedMenuItem.Text); } [Fact] @@ -48,16 +57,107 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal("_Grandchild", clickedMenuItem.Text); } + [PlatformFact(SkipOnOSX = true)] + public void Select_Child_With_Alt_Arrow_Keys() + { + new Actions(_session) + .KeyDown(Keys.Alt).KeyUp(Keys.Alt) + .SendKeys(Keys.Down + Keys.Enter) + .Perform(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Child 1", clickedMenuItem.Text); + } + + [PlatformFact(SkipOnOSX = true)] + public void Select_Grandchild_With_Alt_Arrow_Keys() + { + new Actions(_session) + .KeyDown(Keys.Alt).KeyUp(Keys.Alt) + .SendKeys(Keys.Down + Keys.Down + Keys.Right + Keys.Enter) + .Perform(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Grandchild", clickedMenuItem.Text); + } + + [PlatformFact(SkipOnOSX = true)] + public void Select_Child_With_Alt_Access_Keys() + { + new Actions(_session) + .KeyDown(Keys.Alt).KeyUp(Keys.Alt) + .SendKeys("rc") + .Perform(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Child 1", clickedMenuItem.Text); + } + + [PlatformFact(SkipOnOSX = true)] + public void Select_Grandchild_With_Alt_Access_Keys() + { + new Actions(_session) + .KeyDown(Keys.Alt).KeyUp(Keys.Alt) + .SendKeys("rhg") + .Perform(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Grandchild", clickedMenuItem.Text); + } + + [PlatformFact(SkipOnOSX = true)] + public void Select_Child_With_Click_Arrow_Keys() + { + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + rootMenuItem.SendClick(); + + new Actions(_session) + .SendKeys(Keys.Down + Keys.Enter) + .Perform(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Child 1", clickedMenuItem.Text); + } + + [PlatformFact(SkipOnOSX = true)] + public void Select_Grandchild_With_Click_Arrow_Keys() + { + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + rootMenuItem.SendClick(); + + new Actions(_session) + .SendKeys(Keys.Down + Keys.Down + Keys.Right + Keys.Enter) + .Perform(); + + var clickedMenuItem = _session.FindElementByAccessibilityId("ClickedMenuItem"); + Assert.Equal("_Grandchild", clickedMenuItem.Text); + } + [PlatformFact(SkipOnOSX = true)] public void Child_AcceleratorKey() { var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); - + rootMenuItem.SendClick(); var childMenuItem = _session.FindElementByAccessibilityId("Child1MenuItem"); Assert.Equal("Ctrl+O", childMenuItem.GetAttribute("AcceleratorKey")); } + + [PlatformFact(SkipOnOSX = true)] + public void PointerOver_Does_Not_Steal_Focus() + { + // Issue #7906 + var textBox = _session.FindElementByAccessibilityId("MenuFocusTest"); + textBox.Click(); + + Assert.True(textBox.GetIsFocused()); + + var rootMenuItem = _session.FindElementByAccessibilityId("RootMenuItem"); + rootMenuItem.MovePointerOver(); + + Assert.True(textBox.GetIsFocused()); + } } }