diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj
index c39c81a965..69a80290d1 100644
--- a/src/Avalonia.Input/Avalonia.Input.csproj
+++ b/src/Avalonia.Input/Avalonia.Input.csproj
@@ -4,6 +4,9 @@
Enable
CS8600;CS8602;CS8603
+
+
+
diff --git a/src/Avalonia.Input/ICustomKeyboardNavigation.cs b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
index 3d2927c632..357395c42f 100644
--- a/src/Avalonia.Input/ICustomKeyboardNavigation.cs
+++ b/src/Avalonia.Input/ICustomKeyboardNavigation.cs
@@ -1,4 +1,5 @@
-
+#nullable enable
+
namespace Avalonia.Input
{
///
@@ -6,6 +7,18 @@ namespace Avalonia.Input
///
public interface ICustomKeyboardNavigation
{
- (bool handled, IInputElement next) GetNext(IInputElement element, NavigationDirection direction);
+ ///
+ /// Gets the next element in the specified navigation direction.
+ ///
+ /// The element being navigated from.
+ /// The navigation direction.
+ ///
+ /// 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.
+ ///
+ (bool handled, IInputElement? next) GetNext(IInputElement element, NavigationDirection direction);
}
}
diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs
index 53c9b008ff..6493777105 100644
--- a/src/Avalonia.Input/KeyboardNavigationHandler.cs
+++ b/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()
- .FirstOrDefault();
+ // If there's a custom keyboard navigation handler as an ancestor, use that.
+ var custom = element.FindAncestorOfType(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;
}
///
@@ -94,7 +76,7 @@ namespace Avalonia.Input
/// The direction to move.
/// Any key modifiers active at the time of focus.
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(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;
+ }
}
}
diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs
index d1862c2fa5..12842e4f40 100644
--- a/src/Avalonia.Input/Navigation/TabNavigation.cs
+++ b/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;
diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
index f72d6ba9c9..f9c85ee4ca 100644
--- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Custom.cs
+++ b/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,
diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs
index b69bf990d9..4601dd7e5b 100644
--- a/tests/Avalonia.UnitTests/TestRoot.cs
+++ b/tests/Avalonia.UnitTests/TestRoot.cs
@@ -21,6 +21,7 @@ namespace Avalonia.UnitTests
Renderer = Mock.Of();
LayoutManager = new LayoutManager(this);
IsVisible = true;
+ KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
}
public TestRoot(IControl child)