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, IInputElement element,
NavigationDirection direction) NavigationDirection direction)
{ {
return GetNextPrivate(element, direction, null); element = element ?? throw new ArgumentNullException(nameof(element));
return GetNextPrivate(element, null, direction, null);
} }
private static IInputElement? GetNextPrivate( private static IInputElement? GetNextPrivate(
IInputElement element, IInputElement? element,
IInputRoot? owner,
NavigationDirection direction, NavigationDirection direction,
KeyDeviceType? keyDeviceType) 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. // If there's a custom keyboard navigation handler as an ancestor, use that.
var custom = (element as Visual)?.FindAncestorOfType<ICustomKeyboardNavigation>(true); 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; 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), throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
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(),
};
// If there wasn't a custom navigation handler as an ancestor of the current element, // 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. // 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 ce;
return result; return result;
@ -100,12 +115,7 @@ namespace Avalonia.Input
// TODO12: remove MovePrivate, and make Move return boolean. Or even remove whole KeyboardNavigationHandler. // TODO12: remove MovePrivate, and make Move return boolean. Or even remove whole KeyboardNavigationHandler.
private bool MovePrivate(IInputElement? element, NavigationDirection direction, KeyModifiers keyModifiers, KeyDeviceType? deviceType) private bool MovePrivate(IInputElement? element, NavigationDirection direction, KeyModifiers keyModifiers, KeyDeviceType? deviceType)
{ {
if (element is null && _owner is null) var next = GetNextPrivate(element, _owner, direction, deviceType);
{
return false;
}
var next = GetNextPrivate(element ?? _owner!, direction, deviceType);
if (next != null) if (next != null)
{ {

5
src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs

@ -52,7 +52,8 @@ public partial class XYFocus
internal static InputElement? TryDirectionalFocus( internal static InputElement? TryDirectionalFocus(
NavigationDirection direction, NavigationDirection direction,
IInputElement? element, IInputElement element,
IInputElement? owner,
InputElement? engagedControl, InputElement? engagedControl,
KeyDeviceType? keyDeviceType) KeyDeviceType? keyDeviceType)
{ {
@ -98,7 +99,7 @@ public partial class XYFocus
KeyDeviceType = keyDeviceType, KeyDeviceType = keyDeviceType,
FocusedElementBounds = bounds, FocusedElementBounds = bounds,
UpdateManifold = true, 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.Equal(current, FocusManager.GetFocusManager(current)!.GetFocusedElement());
Assert.False(args.Handled); 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