diff --git a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs index bc9b1cde05..3444a88aba 100644 --- a/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Base/Input/KeyboardNavigationHandler.cs @@ -50,34 +50,49 @@ namespace Avalonia.Input IInputElement element, NavigationDirection direction) { - return GetNextPrivate(element, direction, null); + element = element ?? throw new ArgumentNullException(nameof(element)); + return GetNextPrivate(element, null, direction, null); } private static IInputElement? GetNextPrivate( - IInputElement element, + IInputElement? element, + IInputRoot? owner, NavigationDirection direction, KeyDeviceType? keyDeviceType) { - element = element ?? throw new ArgumentNullException(nameof(element)); + var elementOrOwner = element ?? owner ?? throw new ArgumentNullException(nameof(owner)); // If there's a custom keyboard navigation handler as an ancestor, use that. var custom = (element as Visual)?.FindAncestorOfType(true); - if (custom is not null && HandlePreCustomNavigation(custom, element, direction, out var ce)) + if (custom is not null && HandlePreCustomNavigation(custom, elementOrOwner, direction, out var ce)) return ce; - var result = direction switch + IInputElement? result; + if (direction is NavigationDirection.Next) + { + result = TabNavigation.GetNextTab(elementOrOwner, false); + } + else if (direction is NavigationDirection.Previous) + { + result = TabNavigation.GetPrevTab(elementOrOwner, null, false); + } + else if (direction is NavigationDirection.Up or NavigationDirection.Down + or NavigationDirection.Left or NavigationDirection.Right) + { + // HACK: a window should always have some element focused, + // it seems to be a difference between UWP and Avalonia focus manager implementations. + result = element is null + ? TabNavigation.GetNextTab(elementOrOwner, true) + : XYFocus.TryDirectionalFocus(direction, element, owner, null, keyDeviceType); + } + else { - NavigationDirection.Next => TabNavigation.GetNextTab(element, false), - NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false), - NavigationDirection.Up or NavigationDirection.Down - or NavigationDirection.Left or NavigationDirection.Right - => XYFocus.TryDirectionalFocus(direction, element, null, keyDeviceType), - _ => throw new NotSupportedException(), - }; + throw new ArgumentOutOfRangeException(nameof(direction), direction, null); + } // If there wasn't a custom navigation handler as an ancestor of the current element, // but there is one as an ancestor of the new element, use that. - if (custom is null && HandlePostCustomNavigation(element, result, direction, out ce)) + if (custom is null && HandlePostCustomNavigation(elementOrOwner, result, direction, out ce)) return ce; return result; @@ -100,12 +115,7 @@ namespace Avalonia.Input // TODO12: remove MovePrivate, and make Move return boolean. Or even remove whole KeyboardNavigationHandler. private bool MovePrivate(IInputElement? element, NavigationDirection direction, KeyModifiers keyModifiers, KeyDeviceType? deviceType) { - if (element is null && _owner is null) - { - return false; - } - - var next = GetNextPrivate(element ?? _owner!, direction, deviceType); + var next = GetNextPrivate(element, _owner, direction, deviceType); if (next != null) { diff --git a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs index 7050818341..526668ceca 100644 --- a/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs +++ b/src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs @@ -52,7 +52,8 @@ public partial class XYFocus internal static InputElement? TryDirectionalFocus( NavigationDirection direction, - IInputElement? element, + IInputElement element, + IInputElement? owner, InputElement? engagedControl, KeyDeviceType? keyDeviceType) { @@ -98,7 +99,7 @@ public partial class XYFocus KeyDeviceType = keyDeviceType, FocusedElementBounds = bounds, UpdateManifold = true, - SearchRoot = inputElement.GetVisualRoot() as InputElement + SearchRoot = owner as InputElement ?? inputElement.GetVisualRoot() as InputElement }); } diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs index 8981d35e61..1b8238f4b7 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs @@ -376,4 +376,46 @@ public class KeyboardNavigationTests_XY : ScopedTestBase Assert.Equal(current, FocusManager.GetFocusManager(current)!.GetFocusedElement()); Assert.False(args.Handled); } + + [Fact] + public void Can_Focus_Child_Of_Current_Focused() + { + using var _ = UnitTestApplication.Start(TestServices.FocusableWindow); + + var candidate = new Button() { Height = 20, Width = 20 }; + var window = new Window + { + [XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled, + Content = candidate, + Height = 30 + }; + window.Show(); + + Assert.Null(KeyboardNavigationHandler.GetNext(window, NavigationDirection.Down)); + } + + [Fact] + public void Can_Focus_Any_Element_If_Nothing_Was_Focused() + { + // In the future we might auto-focus any element, but for now XY algorithm should be aware of Avalonia specifics. + using var _ = UnitTestApplication.Start(TestServices.FocusableWindow); + + var candidate = new Button(); + var window = new Window + { + [XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled, + Content = new Canvas + { + Children = { candidate } + } + }; + window.Show(); + + Assert.Null(FocusManager.GetFocusManager(window)!.GetFocusedElement()); + + var args = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Down, Source = window }; + window.RaiseEvent(args); + + Assert.Equal(candidate, FocusManager.GetFocusManager(window)!.GetFocusedElement()); + } }