A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

502 lines
18 KiB

using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Input;
public class KeyboardNavigationTests_XY : ScopedTestBase
{
private static (Canvas canvas, Button[] buttons) CreateXYTestLayout()
{
// 111
// 111
// 111
// 2
// 3
//
// 4
Button x1, x2, x3, x4;
var canvas = new Canvas
{
Width = 500,
Children =
{
(x1 = new Button
{
Content = "A",
[Canvas.LeftProperty] = 50, [Canvas.TopProperty] = 0, Width = 150, Height = 150,
}),
(x2 = new Button
{
Content = "B",
[Canvas.LeftProperty] = 400, [Canvas.TopProperty] = 150, Width = 50, Height = 50,
}),
(x3 = new Button
{
Content = "C",
[Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 200, Width = 50, Height = 50,
}),
(x4 = new Button
{
Content = "D",
[Canvas.LeftProperty] = 100, [Canvas.TopProperty] = 300, Width = 50, Height = 50,
})
}
};
return (canvas, new[] { x1, x2, x3, x4 });
}
[Theory]
[InlineData(1, NavigationDirection.Down, 4)]
[InlineData(1, NavigationDirection.Up, -1)]
[InlineData(1, NavigationDirection.Left, -1)]
[InlineData(1, NavigationDirection.Right, 2)]
// TODO: [InlineData(2, NavigationDirection.Down, 4)] Actual: 3
// TODO: [InlineData(2, NavigationDirection.Up, -1)] Actual 1
[InlineData(2, NavigationDirection.Left, 1)]
[InlineData(2, NavigationDirection.Right, -1)]
[InlineData(3, NavigationDirection.Down, 4)]
// TODO: [InlineData(3, NavigationDirection.Up, 1)] Actual: 2
[InlineData(3, NavigationDirection.Left, -1)]
// TODO: [InlineData(3, NavigationDirection.Right, 4)] Actual: 1
[InlineData(4, NavigationDirection.Down, -1)]
[InlineData(4, NavigationDirection.Up, 1)]
[InlineData(4, NavigationDirection.Left, 3)]
[InlineData(4, NavigationDirection.Right, 2)]
public void Projection_Focus_Depending_On_Direction(int from, NavigationDirection direction, int to)
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var (canvas, buttons) = CreateXYTestLayout();
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = canvas
};
window.Show();
var fromButton = buttons[from - 1];
fromButton.SetValue(XYFocus.UpNavigationStrategyProperty, XYFocusNavigationStrategy.Projection);
fromButton.SetValue(XYFocus.LeftNavigationStrategyProperty, XYFocusNavigationStrategy.Projection);
fromButton.SetValue(XYFocus.RightNavigationStrategyProperty, XYFocusNavigationStrategy.Projection);
fromButton.SetValue(XYFocus.DownNavigationStrategyProperty, XYFocusNavigationStrategy.Projection);
var result = KeyboardNavigationHandler.GetNext(fromButton, direction) as Button;
Assert.Equal(to, result == null ? -1 : Array.IndexOf(buttons, result) + 1);
}
[Theory]
[InlineData(1, NavigationDirection.Down, 3)]
[InlineData(1, NavigationDirection.Up, -1)]
[InlineData(1, NavigationDirection.Left, 3)]
[InlineData(1, NavigationDirection.Right, 2)]
[InlineData(2, NavigationDirection.Down, 3)]
[InlineData(2, NavigationDirection.Up, 1)]
[InlineData(2, NavigationDirection.Left, 1)]
[InlineData(2, NavigationDirection.Right, -1)]
[InlineData(3, NavigationDirection.Down, 4)]
[InlineData(3, NavigationDirection.Up, 1)]
[InlineData(3, NavigationDirection.Left, -1)]
[InlineData(3, NavigationDirection.Right, 1)]
[InlineData(4, NavigationDirection.Down, -1)]
[InlineData(4, NavigationDirection.Up, 3)]
[InlineData(4, NavigationDirection.Left, 3)]
[InlineData(4, NavigationDirection.Right, 2)]
public void RectilinearDistance_Focus_Depending_On_Direction(int from, NavigationDirection direction, int to)
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var (canvas, buttons) = CreateXYTestLayout();
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = canvas
};
window.Show();
var fromButton = buttons[from - 1];
fromButton.SetValue(XYFocus.UpNavigationStrategyProperty, XYFocusNavigationStrategy.RectilinearDistance);
fromButton.SetValue(XYFocus.LeftNavigationStrategyProperty, XYFocusNavigationStrategy.RectilinearDistance);
fromButton.SetValue(XYFocus.RightNavigationStrategyProperty, XYFocusNavigationStrategy.RectilinearDistance);
fromButton.SetValue(XYFocus.DownNavigationStrategyProperty, XYFocusNavigationStrategy.RectilinearDistance);
var result = KeyboardNavigationHandler.GetNext(fromButton, direction) as Button;
Assert.Equal(to, result == null ? -1 : Array.IndexOf(buttons, result) + 1);
}
[Theory]
[InlineData(1, NavigationDirection.Down, 2)]
[InlineData(1, NavigationDirection.Up, -1)]
[InlineData(1, NavigationDirection.Left, 3)]
[InlineData(1, NavigationDirection.Right, 2)]
[InlineData(2, NavigationDirection.Down, 3)]
[InlineData(2, NavigationDirection.Up, 1)]
[InlineData(2, NavigationDirection.Left, 1)]
[InlineData(2, NavigationDirection.Right, -1)]
[InlineData(3, NavigationDirection.Down, 4)]
[InlineData(3, NavigationDirection.Up, 2)]
[InlineData(3, NavigationDirection.Left, -1)]
[InlineData(3, NavigationDirection.Right, 1)]
[InlineData(4, NavigationDirection.Down, -1)]
[InlineData(4, NavigationDirection.Up, 3)]
[InlineData(4, NavigationDirection.Left, 3)]
[InlineData(4, NavigationDirection.Right, 2)]
public void NavigationDirectionDistance_Focus_Depending_On_Direction(int from, NavigationDirection direction, int to)
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var (canvas, buttons) = CreateXYTestLayout();
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = canvas
};
window.Show();
var fromButton = buttons[from - 1];
fromButton.SetValue(XYFocus.UpNavigationStrategyProperty, XYFocusNavigationStrategy.NavigationDirectionDistance);
fromButton.SetValue(XYFocus.LeftNavigationStrategyProperty, XYFocusNavigationStrategy.NavigationDirectionDistance);
fromButton.SetValue(XYFocus.RightNavigationStrategyProperty, XYFocusNavigationStrategy.NavigationDirectionDistance);
fromButton.SetValue(XYFocus.DownNavigationStrategyProperty, XYFocusNavigationStrategy.NavigationDirectionDistance);
var result = KeyboardNavigationHandler.GetNext(fromButton, direction) as Button;
Assert.Equal(to, result == null ? -1 : Array.IndexOf(buttons, result) + 1);
}
[Fact]
public void Uses_XY_Directional_Overrides()
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var left = new Button();
var right = new Button();
var up = new Button();
var down = new Button();
var center = new Button
{
[XYFocus.LeftProperty] = left,
[XYFocus.RightProperty] = right,
[XYFocus.UpProperty] = up,
[XYFocus.DownProperty] = down,
};
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = new Canvas
{
Children =
{
left, right, up, down, center
}
}
};
window.Show();
Assert.Equal(left, KeyboardNavigationHandler.GetNext(center, NavigationDirection.Left));
Assert.Equal(right, KeyboardNavigationHandler.GetNext(center, NavigationDirection.Right));
Assert.Equal(up, KeyboardNavigationHandler.GetNext(center, NavigationDirection.Up));
Assert.Equal(down, KeyboardNavigationHandler.GetNext(center, NavigationDirection.Down));
}
[Fact]
public void XY_Directional_Override_Discarded_If_Not_Part_Of_The_Same_Root()
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var left = new Button();
var center = new Button
{
[XYFocus.LeftProperty] = left
};
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = center
};
window.Show();
Assert.Null(KeyboardNavigationHandler.GetNext(center, NavigationDirection.Left));
}
[Fact]
public void Parent_Can_Override_Navigation_When_Directional_Is_Set()
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
// With double stack panel layout we have something like this:
// [ [ EXPECTED, CURRENT ] CANDIDATE ]
// Where normally from Current focus would go to the Candidate.
// But since we set `XYFocus.Right` on nested StackPanel, it should be used instead.
// But ONLY if Candidate isn't part of that nested StackPanel (it isn't).
var current = new Button();
var candidate = new Button();
var expectedOverride = new Button();
var parent = new StackPanel
{
Orientation = Orientation.Horizontal,
Children = { expectedOverride, current },
[XYFocus.RightProperty] = expectedOverride,
// Property value to simplify test.
[XYFocus.RightNavigationStrategyProperty] = XYFocusNavigationStrategy.RectilinearDistance
};
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = new StackPanel
{
Orientation = Orientation.Horizontal,
Children = { parent, candidate }
}
};
window.Show();
Assert.Equal(expectedOverride, KeyboardNavigationHandler.GetNext(current, NavigationDirection.Right));
}
[Fact]
public void Clipped_Element_Should_Not_Be_Focused()
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var current = new Button() { Height = 20 };
var candidate = new Button() { Height = 20 };
var parent = new StackPanel
{
Orientation = Orientation.Vertical,
Spacing = 20,
Children = { current, candidate }
};
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = parent,
Height = 30
};
window.Show();
Assert.Null(KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down));
}
[Fact]
public void Clipped_Element_Should_Not_Focused_If_Inside_Of_ScrollViewer()
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var current = new Button() { Height = 20 };
var candidate = new Button() { Height = 20 };
var parent = new StackPanel
{
Orientation = Orientation.Vertical,
Spacing = 20,
Children = { current, candidate }
};
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = new ScrollViewer
{
Content = parent
},
Height = 30
};
window.Show();
Assert.Equal(candidate, KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down));
}
[Theory]
[InlineData(Key.Left, NavigationDirection.Left)]
[InlineData(Key.Right, NavigationDirection.Right)]
[InlineData(Key.Up, NavigationDirection.Up)]
[InlineData(Key.Down, NavigationDirection.Down)]
public void Arrow_Key_Should_Focus_Element(Key key, NavigationDirection direction)
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var candidate = new Button();
var current = new Button();
current[direction switch
{
NavigationDirection.Left => XYFocus.LeftProperty,
NavigationDirection.Right => XYFocus.RightProperty,
NavigationDirection.Up => XYFocus.UpProperty,
NavigationDirection.Down => XYFocus.DownProperty,
_ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null)
}] = candidate;
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = new Canvas
{
Children = { current, candidate }
}
};
window.Show();
Assert.True(current.Focus());
var args = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = key, Source = current };
window.RaiseEvent(args);
Assert.Equal(candidate, FocusManager.GetFocusManager(current)!.GetFocusedElement());
Assert.True(args.Handled);
}
[Theory]
[InlineData(Key.Left)]
[InlineData(Key.Right)]
[InlineData(Key.Up)]
[InlineData(Key.Down)]
public void Arrow_Key_Should_Not_Be_Handled_If_No_Focus(Key key)
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var current = new Button();
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = new Canvas
{
Children = { current }
}
};
window.Show();
Assert.True(current.Focus());
var args = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = key, Source = current };
window.RaiseEvent(args);
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());
}
[Fact]
public void Cannot_Focus_Across_XYFocus_Boundaries()
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var current = new Button() { Height = 20 };
var candidate = new Button() { Height = 20 };
var currentParent = new StackPanel
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Orientation = Orientation.Vertical,
Spacing = 20,
Children = { current }
};
var candidateParent = new StackPanel
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Orientation = Orientation.Vertical,
Spacing = 20,
Children = { candidate }
};
var grandparent = new StackPanel
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Disabled,
Orientation = Orientation.Vertical,
Spacing = 20,
Children = { currentParent, candidateParent }
};
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = grandparent,
Height = 300
};
window.Show();
Assert.Null(KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down));
}
[Fact]
public void XYFocus_Skips_Effectively_Disabled_Controls()
{
using var _ = UnitTestApplication.Start(TestServices.FocusableWindow);
var current = new TestControl() { Height = 20, Width = 20, IsEnabled = true, IsVisible = true, Focusable = true, ShouldEnable = true};
var disabled = new TestControl() { Height = 20, Width = 20, IsEnabled = true, IsVisible = true, Focusable = true, ShouldEnable = false };
var candidate = new TestControl() { Height = 20, Width = 20, IsEnabled = true, IsVisible = true, Focusable = true, ShouldEnable = true };
var parent = new StackPanel
{
Orientation = Orientation.Vertical,
Spacing = 20,
Children = { current, disabled, candidate }
};
var window = new Window
{
[XYFocus.NavigationModesProperty] = XYFocusNavigationModes.Enabled,
Content = parent,
Height = 300
};
window.Show();
Assert.Equal(candidate, KeyboardNavigationHandler.GetNext(current, NavigationDirection.Down));
}
private class TestControl : Decorator
{
private bool _shouldEnable;
public bool ShouldEnable
{
get => _shouldEnable;
set { _shouldEnable = value; UpdateIsEffectivelyEnabled(); }
}
protected override bool IsEnabledCore => IsEnabled && _shouldEnable;
}
}