Browse Source

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 <julien@lebosquain.net>
pull/21936/head
Autumn 2 months ago
committed by GitHub
parent
commit
92ea5b7a7d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 23
      src/Avalonia.Base/Input/AccessKeyHandler.cs
  2. 96
      tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs

23
src/Avalonia.Base/Input/AccessKeyHandler.cs

@ -176,9 +176,9 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
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
/// <param name="e">The event args.</param>
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
}
/// <summary>
/// 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.
/// </summary>
/// <param name="owner">The owner to check.</param>
/// <returns>If focused element is decendant of owner <c>true</c>, otherwise <c>false</c>. </returns>
private static bool IsFocusWithinOwner(IInputElement owner)
/// <param name="source">The source of the key event, representing the effective focused element.</param>
/// <returns>If <paramref name="source"/> is <paramref name="owner"/> or a descendant of it, <c>true</c>, otherwise <c>false</c>.</returns>
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);
}
/// <summary>

96
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

Loading…
Cancel
Save