Browse Source

Fix keyboard nav with partially visible items.

Steven Kirk 9 years ago
parent
commit
cba5200645

+ 6 - 1
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -143,6 +143,7 @@ namespace Avalonia.Controls.Presenters
         public override IControl GetControlInDirection(FocusNavigationDirection direction, IControl from)
         {
             var generator = Owner.ItemContainerGenerator;
+            var panel = VirtualizingPanel;
             var itemIndex = generator.IndexFromContainer(from);
 
             if (itemIndex == -1)
@@ -167,8 +168,12 @@ namespace Avalonia.Controls.Presenters
 
             if (newItemIndex >= 0 && newItemIndex < ItemCount)
             {
+                // Get the index of the first and last fully visible items (i.e. excluding any
+                // partially visible item at the beginning or end).
+                var firstIndex = panel.PixelOffset == 0 ? FirstIndex : FirstIndex + 1;
+                var lastIndex = (FirstIndex + ViewportValue) - 1;
 
-                if (newItemIndex < FirstIndex || newItemIndex >= NextIndex)
+                if (newItemIndex < firstIndex || newItemIndex > lastIndex)
                 {
                     OffsetValue += newItemIndex - itemIndex;
                     InvalidateScroll();

+ 86 - 10
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

@@ -10,6 +10,7 @@ using Avalonia.Controls.Generators;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
+using Avalonia.Input;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests.Presenters
@@ -131,7 +132,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 95));
@@ -162,7 +163,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Inserting_Items_Should_Update_Containers()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 100));
@@ -187,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Removing_First_Materialized_Item_Should_Update_Containers()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 100));
@@ -209,7 +210,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Removing_Items_From_Middle_Should_Update_Containers_When_All_Items_Visible()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 200));
@@ -234,7 +235,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Removing_Last_Item_Should_Update_Containers_When_All_Items_Visible()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 200));
@@ -258,7 +259,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Removing_Items_When_Scrolled_To_End_Should_Recyle_Containers_At_Top()
         {
-            var target = CreateTarget(itemCount: 20, useAvaloniaList: true);
+            var target = CreateTarget(useAvaloniaList: true);
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 100));
@@ -282,7 +283,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Removing_Items_When_Scrolled_To_Near_End_Should_Recycle_Containers_At_Bottom_And_Top()
         {
-            var target = CreateTarget(itemCount: 20, useAvaloniaList: true);
+            var target = CreateTarget(useAvaloniaList: true);
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 100));
@@ -308,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Replacing_Items_Should_Update_Containers()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 100));
@@ -329,7 +330,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Moving_Items_Should_Update_Containers()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 100));
@@ -353,7 +354,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
         [Fact]
         public void Setting_Items_To_Null_Should_Remove_Containers()
         {
-            var target = CreateTarget(itemCount: 20);
+            var target = CreateTarget();
 
             target.ApplyTemplate();
             target.Measure(new Size(100, 100));
@@ -370,6 +371,81 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Empty(target.Panel.Children);
         }
 
+        public class Vertical
+        {
+            [Fact]
+            public void GetControlInDirection_Down_Should_Return_Existing_Container_If_Materialized()
+            {
+                var target = CreateTarget();
+
+                target.ApplyTemplate();
+                target.Measure(new Size(100, 100));
+                target.Arrange(new Rect(0, 0, 100, 100));
+
+                var from = target.Panel.Children[5];
+                var result = ((ILogicalScrollable)target).GetControlInDirection(
+                    FocusNavigationDirection.Down,
+                    from);
+
+                Assert.Same(target.Panel.Children[6], result);
+            }
+
+            [Fact]
+            public void GetControlInDirection_Down_Should_Scroll_If_Necessary()
+            {
+                var target = CreateTarget();
+
+                target.ApplyTemplate();
+                target.Measure(new Size(100, 100));
+                target.Arrange(new Rect(0, 0, 100, 100));
+
+                var from = target.Panel.Children[9];
+                var result = ((ILogicalScrollable)target).GetControlInDirection(
+                    FocusNavigationDirection.Down,
+                    from);
+
+                Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
+                Assert.Same(target.Panel.Children[9], result);
+            }
+
+            [Fact]
+            public void GetControlInDirection_Down_Should_Scroll_If_Partially_Visible()
+            {
+                var target = CreateTarget();
+
+                target.ApplyTemplate();
+                target.Measure(new Size(100, 95));
+                target.Arrange(new Rect(0, 0, 100, 95));
+
+                var from = target.Panel.Children[8];
+                var result = ((ILogicalScrollable)target).GetControlInDirection(
+                    FocusNavigationDirection.Down,
+                    from);
+
+                Assert.Equal(new Vector(0, 1), ((ILogicalScrollable)target).Offset);
+                Assert.Same(target.Panel.Children[8], result);
+            }
+
+            [Fact]
+            public void GetControlInDirection_Up_Should_Scroll_If_Partially_Visible_Is_Currently_Shown()
+            {
+                var target = CreateTarget();
+
+                target.ApplyTemplate();
+                target.Measure(new Size(100, 95));
+                target.Arrange(new Rect(0, 0, 100, 95));
+                ((ILogicalScrollable)target).Offset = new Vector(0, 11);
+
+                var from = target.Panel.Children[1];
+                var result = ((ILogicalScrollable)target).GetControlInDirection(
+                    FocusNavigationDirection.Up,
+                    from);
+
+                Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset);
+                Assert.Same(target.Panel.Children[0], result);
+            }
+        }
+
         public class WithContainers
         {
             [Fact]