diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 97d08b5620..739ff72e51 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -176,9 +176,9 @@ namespace Avalonia.Input /// The event args. protected virtual void OnPreviewKeyDown(object? sender, KeyEventArgs e) { - // if the owner (IInputRoot) does not have the keyboard focus, ignore all keyboard events + // if the event did not originate from within the owner (IInputRoot), ignore all keyboard events // KeyboardDevice.IsKeyboardFocusWithin in case of a PopupRoot seems to only work once, so we created our own - var isFocusWithinOwner = IsFocusWithinOwner(_owner!); + var isFocusWithinOwner = IsFocusWithinOwner(_owner!, e.Source as IInputElement); if (!isFocusWithinOwner) return; @@ -226,9 +226,9 @@ namespace Avalonia.Input /// The event args. protected virtual void OnKeyDown(object? sender, KeyEventArgs e) { - // if the owner (IInputRoot) does not have the keyboard focus, ignore all keyboard events + // if the event did not originate from within the owner (IInputRoot), ignore all keyboard events // KeyboardDevice.IsKeyboardFocusWithin in case of a PopupRoot seems to only work once, so we created our own - var isFocusWithinOwner = IsFocusWithinOwner(_owner!); + var isFocusWithinOwner = IsFocusWithinOwner(_owner!, e.Source as IInputElement); if (!isFocusWithinOwner) return; @@ -450,18 +450,17 @@ namespace Avalonia.Input } /// - /// Checks if the focused element is a descendent of the owner. + /// Checks if the event's source is the owner itself, or a descendant of it. /// /// The owner to check. - /// If focused element is decendant of owner true, otherwise false. - private static bool IsFocusWithinOwner(IInputElement owner) + /// The source of the key event, representing the effective focused element. + /// If is or a descendant of it, true, otherwise false. + private static bool IsFocusWithinOwner(InputElement owner, IInputElement? source) { - var focusedElement = KeyboardDevice.Instance?.FocusedElement; - if (focusedElement is not InputElement inputElement) + if (source is not Visual sourceVisual) return false; - - var isAncestorOf = owner is Visual root && root.IsVisualAncestorOf(inputElement); - return isAncestorOf; + + return ReferenceEquals(sourceVisual, owner) || owner.IsVisualAncestorOf(sourceVisual); } /// diff --git a/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs b/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs index 166b777ba1..557459d9af 100644 --- a/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs @@ -131,11 +131,12 @@ namespace Avalonia.Base.UnitTests.Input KeyUp(root, Key.A, "a", KeyModifiers.Alt); KeyUp(root, Key.LeftAlt); - // This differs from WPF which doesn't raise the `A` key event, but matches UWP. + // AccessKeyHandler marks the Alt+A KeyDown as Handled once it matches a registered + // access key, so a plain KeyDown subscriber doesn't see it. KeyUp is unaffected: only + // KeyUp for the Alt key itself is handled, not arbitrary registered access keys. Assert.Equal(new[] { "KeyDown LeftAlt", - "KeyDown A", "KeyUp A", "KeyUp LeftAlt", }, events); @@ -283,6 +284,97 @@ namespace Avalonia.Base.UnitTests.Input } } + [Fact] + public void Should_Raise_AccessKey_When_Nothing_Is_Focused() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var button = new Button(); + var root = new TestRoot(button); + var target = new AccessKeyHandler(); + var raised = 0; + + Assert.Null(KeyboardDevice.Instance?.FocusedElement); + + target.SetOwner(root); + target.Register("A", button); + button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised); + + KeyDown(root, Key.LeftAlt); + KeyDown(root, Key.A, "a", KeyModifiers.Alt); + + Assert.Equal(1, raised); + } + } + + [Fact] + public void Should_Raise_AccessKey_When_Owner_Itself_Is_Focused() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var button = new Button(); + var root = new TestRoot(button); + var target = new AccessKeyHandler(); + var raised = 0; + + KeyboardDevice.Instance?.SetFocusedElement(root, NavigationMethod.Unspecified, KeyModifiers.None); + + target.SetOwner(root); + target.Register("A", button); + button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised); + + KeyDown(root, Key.LeftAlt); + KeyDown(root, Key.A, "a", KeyModifiers.Alt); + + Assert.Equal(1, raised); + } + } + + [Fact] + public void Should_Open_MainMenu_On_Alt_KeyUp_When_Nothing_Is_Focused() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var target = new AccessKeyHandler(); + var menu = new FakeMenu(); + var root = new TestRoot(menu); + + Assert.Null(KeyboardDevice.Instance?.FocusedElement); + + target.SetOwner(root); + target.MainMenu = menu; + + KeyDown(root, Key.LeftAlt); + Assert.Equal(0, menu.TimesOpenCalled); + + KeyUp(root, Key.LeftAlt); + Assert.Equal(1, menu.TimesOpenCalled); + } + } + + [Fact] + public void Should_Raise_AccessKey_When_Focus_Is_On_Descendant() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var button = new Button(); + var root = new TestRoot(button); + var target = new AccessKeyHandler(); + var raised = 0; + + KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None); + + target.SetOwner(root); + target.Register("A", button); + button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised); + + KeyDown(button, Key.LeftAlt); + KeyDown(button, Key.A, "a", KeyModifiers.Alt); + + Assert.Equal(1, raised); + } + } + private static void KeyDown(IInputElement target, Key key, string? keySymbol = null, KeyModifiers modifiers = KeyModifiers.None) { target.RaiseEvent(new KeyEventArgs