From 92ea5b7a7dea234edfdc6a61d5e8f7912911cd43 Mon Sep 17 00:00:00 2001 From: Autumn <80438149+AutumnYuanc@users.noreply.github.com> Date: Fri, 7 Aug 2026 17:50:41 +0800 Subject: [PATCH] fix(input): fix AccessKeyHandler when no descendant control has focus (#21920) * fix(input): fix AccessKeyHandler when no descendant control has focus - Update IsFocusWithinOwner to use routed event's Source instead of re-querying KeyboardDevice.Instance.FocusedElement - Allow access keys (Alt/mnemonics) to work immediately after Window opens when FocusedElement is null or owner itself - Update shared test helpers in AccessKeyHandlerTests to populate event Source Fixes #21806 * fix(input): address review feedback on AccessKeyHandler focus fix - Type IsFocusWithinOwner's owner parameter as InputElement (matching _owner) and drop the now-redundant `is Visual` check - Fix indentation in IsFocusWithinOwner - Add Should_Raise_AccessKey_When_Focus_Is_On_Descendant, covering the IsVisualAncestorOf branch by raising KeyDown/KeyUp on a descendant control instead of the owner - Revert the no-op Source assignment in the KeyDown/KeyUp test helpers - Shorten the comment in Should_Raise_Key_Events_For_Registered_Access_Key --------- Co-authored-by: Julien Lebosquain --- src/Avalonia.Base/Input/AccessKeyHandler.cs | 23 +++-- .../Input/AccessKeyHandlerTests.cs | 96 ++++++++++++++++++- 2 files changed, 105 insertions(+), 14 deletions(-) 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