Browse Source

Make XY focus navigation less broken, when there is no starting focused control

xy-focus-and-tvos
Max Katz 2 years ago
parent
commit
6ea95dbdb5
  1. 48
      src/Avalonia.Base/Input/KeyboardNavigationHandler.cs
  2. 5
      src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs
  3. 42
      tests/Avalonia.Base.UnitTests/Input/KeyboardNavigationTests_XY.cs

48
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<ICustomKeyboardNavigation>(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)
{

5
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
});
}

42
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());
}
}

Loading…
Cancel
Save