1
0
Эх сурвалжийг харах

Make custom keyboard navigation work again.

Steven Kirk 4 жил өмнө
parent
commit
ec51318315

+ 3 - 0
src/Avalonia.Input/Avalonia.Input.csproj

@@ -4,6 +4,9 @@
     <Nullable>Enable</Nullable>
     <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
   </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
+  </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
     <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />

+ 15 - 2
src/Avalonia.Input/ICustomKeyboardNavigation.cs

@@ -1,4 +1,5 @@
-
+#nullable enable
+
 namespace Avalonia.Input
 {
     /// <summary>
@@ -6,6 +7,18 @@ namespace Avalonia.Input
     /// </summary>
     public interface ICustomKeyboardNavigation
     {
-        (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction);
+        /// <summary>
+        /// Gets the next element in the specified navigation direction.
+        /// </summary>
+        /// <param name="element">The element being navigated from.</param>
+        /// <param name="direction">The navigation direction.</param>
+        /// <returns>
+        /// A tuple consisting of:
+        /// - A boolean indicating whether the request was handled. If false is returned then 
+        ///   custom navigation will be ignored and default navigation will take place.
+        /// - If handled is true: the next element in the navigation direction, or null if default
+        ///   navigation should continue outside the element.
+        /// </returns>
+        (bool handled, IInputElement? next) GetNext(IInputElement element, NavigationDirection direction);
     }
 }

+ 79 - 32
src/Avalonia.Input/KeyboardNavigationHandler.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Avalonia.Input.Navigation;
 using Avalonia.VisualTree;
@@ -48,43 +49,24 @@ namespace Avalonia.Input
         {
             element = element ?? throw new ArgumentNullException(nameof(element));
 
-            var customHandler = element.GetSelfAndVisualAncestors()
-                .OfType<ICustomKeyboardNavigation>()
-                .FirstOrDefault();
+            // If there's a custom keyboard navigation handler as an ancestor, use that.
+            var custom = element.FindAncestorOfType<ICustomKeyboardNavigation>(true);
+            if (custom is object && HandlePreCustomNavigation(custom, element, direction, out var ce))
+                return ce;
 
-            if (customHandler != null)
-            {
-                var (handled, next) = customHandler.GetNext(element, direction);
-
-                if (handled)
-                {
-                    if (next != null)
-                    {
-                        return next;
-                    }
-                    else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
-                    {
-                        var e = (IInputElement)customHandler;
-                        return direction switch
-                        {
-                            NavigationDirection.Next => TabNavigation.GetNextTab(e, false),
-                            NavigationDirection.Previous => TabNavigation.GetPrevTab(e, null, false),
-                            _ => throw new NotSupportedException(),
-                        };
-                    }
-                    else
-                    {
-                        return null;
-                    }
-                }
-            }
-
-            return direction switch
+            var result = direction switch
             {
                 NavigationDirection.Next => TabNavigation.GetNextTab(element, false),
                 NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false),
                 _ => throw new NotSupportedException(),
             };
+
+            // 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))
+                return ce;
+
+            return result;
         }
 
         /// <summary>
@@ -94,7 +76,7 @@ namespace Avalonia.Input
         /// <param name="direction">The direction to move.</param>
         /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
         public void Move(
-            IInputElement element, 
+            IInputElement element,
             NavigationDirection direction,
             KeyModifiers keyModifiers = KeyModifiers.None)
         {
@@ -128,5 +110,70 @@ namespace Avalonia.Input
                 e.Handled = true;
             }
         }
+
+        private static bool HandlePreCustomNavigation(
+            ICustomKeyboardNavigation customHandler,
+            IInputElement element,
+            NavigationDirection direction,
+            [NotNullWhen(true)] out IInputElement? result)
+        {
+            if (customHandler != null)
+            {
+                var (handled, next) = customHandler.GetNext(element, direction);
+
+                if (handled)
+                {
+                    if (next != null)
+                    {
+                        result = next;
+                        return true;
+                    }
+                    else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
+                    {
+                        var r = direction switch
+                        {
+                            NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler),
+                            NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler),
+                            _ => throw new NotSupportedException(),
+                        };
+
+                        if (r is object)
+                        {
+                            result = r;
+                            return true;
+                        }
+                    }
+                }
+            }
+
+            result = null;
+            return false;
+        }
+
+        private static bool HandlePostCustomNavigation(
+            IInputElement element,
+            IInputElement? newElement,
+            NavigationDirection direction,
+            [NotNullWhen(true)] out IInputElement? result)
+        {
+            if (newElement is object)
+            {
+                var customHandler = newElement.FindAncestorOfType<ICustomKeyboardNavigation>(true);
+
+                if (customHandler is object)
+                {
+                    var (handled, next) = customHandler.GetNext(element, direction);
+
+                    if (handled && next is object)
+                    {
+                        result = next;
+                        return true;
+                    }
+                }
+            }
+
+            result = null;
+            return false;
+        }
     }
 }

+ 26 - 0
src/Avalonia.Input/Navigation/TabNavigation.cs

@@ -78,6 +78,19 @@ namespace Avalonia.Input.Navigation
             return null;
         }
 
+        public static IInputElement? GetNextTabOutside(ICustomKeyboardNavigation e)
+        {
+            if (e is IInputElement container)
+            {
+                var last = GetLastInTree(container);
+
+                if (last is object)
+                    return GetNextTab(last, false);
+            }
+
+            return null;
+        }
+
         public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly)
         {
             if (e is null && container is null)
@@ -171,6 +184,19 @@ namespace Avalonia.Input.Navigation
             return null;
         }
 
+        public static IInputElement? GetPrevTabOutside(ICustomKeyboardNavigation e)
+        {
+            if (e is IInputElement container)
+            {
+                var first = GetFirstChild(container);
+
+                if (first is object)
+                    return GetPrevTab(first, null, false);
+            }
+
+            return null;
+        }
+
         private static IInputElement? FocusedElement(IInputElement e)
         {
             var iie = e;

+ 33 - 0
tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs

@@ -95,6 +95,7 @@ namespace Avalonia.Input.UnitTests
 
             var root = new StackPanel
             {
+                [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
                 Children =
                 {
                     target,
@@ -125,6 +126,7 @@ namespace Avalonia.Input.UnitTests
 
             var root = new StackPanel
             {
+                [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
                 Children =
                 {
                     (current = new Button { Content = "Outside" }),
@@ -137,6 +139,36 @@ namespace Avalonia.Input.UnitTests
             Assert.Same(next, result);
         }
 
+        [Fact]
+        public void ShiftTab_Should_Navigate_Outside_When_Null_Returned_As_Next()
+        {
+            Button current;
+            Button next;
+            var target = new CustomNavigatingStackPanel
+            {
+                Children =
+                {
+                    new Button { Content = "Button 1" },
+                    (current = new Button { Content = "Button 2" }),
+                    new Button { Content = "Button 3" },
+                },
+            };
+
+            var root = new StackPanel
+            {
+                [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
+                Children =
+                {
+                    target,
+                    (next = new Button { Content = "Outside" }),
+                }
+            };
+
+            var result = KeyboardNavigationHandler.GetNext(current, NavigationDirection.Previous);
+
+            Assert.Same(next, result);
+        }
+
         [Fact]
         public void Tab_Should_Navigate_Outside_When_Null_Returned_As_Next()
         {
@@ -154,6 +186,7 @@ namespace Avalonia.Input.UnitTests
 
             var root = new StackPanel
             {
+                [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
                 Children =
                 {
                     target,

+ 1 - 0
tests/Avalonia.UnitTests/TestRoot.cs

@@ -21,6 +21,7 @@ namespace Avalonia.UnitTests
             Renderer = Mock.Of<IRenderer>();
             LayoutManager = new LayoutManager(this);
             IsVisible = true;
+            KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
         }
 
         public TestRoot(IControl child)