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