diff --git a/src/Avalonia.Base/Input/FindNextElementOptions.cs b/src/Avalonia.Base/Input/FindNextElementOptions.cs
index 72d83ec419..ee9c2e6fef 100644
--- a/src/Avalonia.Base/Input/FindNextElementOptions.cs
+++ b/src/Avalonia.Base/Input/FindNextElementOptions.cs
@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Avalonia.Input
+namespace Avalonia.Input
{
///
/// Provides options to customize the behavior when identifying the next element to focus
@@ -12,14 +6,28 @@ namespace Avalonia.Input
///
public sealed class FindNextElementOptions
{
+ ///
+ /// Gets or sets the element that will be treated as the starting point of the search
+ /// for the next focusable element. This does not need to be the element that is
+ /// currently focused. If null, is used.
+ ///
+ public IInputElement? FocusedElement { get; init; }
+
///
/// Gets or sets the root within which the search for the next
/// focusable element will be conducted.
///
///
+ ///
/// This property defines the boundary for focus navigation operations. It determines the root element
/// in the visual tree under which the focusable item search is performed. If not specified, the search
/// will default to the current scope.
+ ///
+ ///
+ /// This option is only used with , ,
+ /// , and . It is ignored for other
+ /// directions.
+ ///
///
public InputElement? SearchRoot { get; init; }
@@ -27,6 +35,11 @@ namespace Avalonia.Input
/// Gets or sets the rectangular region within the visual hierarchy that will be excluded
/// from consideration during focus navigation.
///
+ ///
+ /// This option is only used with , ,
+ /// , and . It is ignored for other
+ /// directions.
+ ///
public Rect ExclusionRect { get; init; }
///
@@ -35,12 +48,22 @@ namespace Avalonia.Input
/// which can be used as a preferred or prioritized target when navigating focus.
/// It can be null if no specific hint region is provided.
///
+ ///
+ /// This option is only used with , ,
+ /// , and . It is ignored for other
+ /// directions.
+ ///
public Rect? FocusHintRectangle { get; init; }
///
/// Specifies an optional override for the navigation strategy used in XY focus navigation.
/// This property allows customizing the focus movement behavior when navigating between UI elements.
///
+ ///
+ /// This option is only used with , ,
+ /// , and . It is ignored for other
+ /// directions.
+ ///
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; }
///
@@ -49,6 +72,11 @@ namespace Avalonia.Input
/// the navigation logic disregards obstructions that may block a potential
/// focus target, allowing elements behind such obstructions to be considered.
///
+ ///
+ /// This option is only used with , ,
+ /// , and . It is ignored for other
+ /// directions.
+ ///
public bool IgnoreOcclusivity { get; init; }
}
}
diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs
index dc62171f48..a273ff6d89 100644
--- a/src/Avalonia.Base/Input/FocusManager.cs
+++ b/src/Avalonia.Base/Input/FocusManager.cs
@@ -172,7 +172,7 @@ namespace Avalonia.Input
ValidateDirection(direction);
var focusOptions = ToFocusOptions(options, true);
- var result = FindAndSetNextFocus(direction, focusOptions);
+ var result = FindAndSetNextFocus(options?.FocusedElement ?? Current, direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
@@ -319,7 +319,7 @@ namespace Avalonia.Input
ValidateDirection(direction);
var focusOptions = ToFocusOptions(options, false);
- var result = FindNextFocus(direction, focusOptions);
+ var result = FindNextFocus(options?.FocusedElement ?? Current, direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
@@ -368,27 +368,29 @@ namespace Avalonia.Input
return focusOptions;
}
- internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true)
+ private IInputElement? FindNextFocus(
+ IInputElement? focusedElement,
+ NavigationDirection direction,
+ XYFocusOptions focusOptions,
+ bool updateManifolds = true)
{
- IInputElement? nextFocusedElement = null;
+ IInputElement? nextFocusedElement;
- var currentlyFocusedElement = Current;
-
- if (direction is NavigationDirection.Previous or NavigationDirection.Next || currentlyFocusedElement == null)
+ if (direction is NavigationDirection.Previous or NavigationDirection.Next || focusedElement == null)
{
var isReverse = direction == NavigationDirection.Previous;
- nextFocusedElement = ProcessTabStopInternal(isReverse, true);
+ nextFocusedElement = ProcessTabStopInternal(focusedElement, isReverse, true);
}
else
{
- if (currentlyFocusedElement is InputElement inputElement &&
+ if (focusedElement is InputElement inputElement &&
XYFocus.GetBoundsForRanking(inputElement, focusOptions.IgnoreClipping) is { } bounds)
{
focusOptions.FocusedElementBounds = bounds;
}
nextFocusedElement = _xyFocus.GetNextFocusableElement(direction,
- currentlyFocusedElement as InputElement,
+ focusedElement as InputElement,
null,
updateManifolds,
focusOptions);
@@ -509,14 +511,14 @@ namespace Avalonia.Input
return lastFocus;
}
- private IInputElement? ProcessTabStopInternal(bool isReverse, bool queryOnly)
+ private IInputElement? ProcessTabStopInternal(IInputElement? focusedElement, bool isReverse, bool queryOnly)
{
IInputElement? newTabStop = null;
- var defaultCandidateTabStop = GetTabStopCandidateElement(isReverse, queryOnly, out var didCycleFocusAtRootVisualScope);
+ var defaultCandidateTabStop = GetTabStopCandidateElement(focusedElement, isReverse, queryOnly, out var didCycleFocusAtRootVisualScope);
var isTabStopOverriden = InputElement.ProcessTabStop(_contentRoot,
- Current,
+ focusedElement,
defaultCandidateTabStop,
isReverse,
didCycleFocusAtRootVisualScope,
@@ -535,24 +537,27 @@ namespace Avalonia.Input
return newTabStop;
}
- private IInputElement? GetTabStopCandidateElement(bool isReverse, bool queryOnly, out bool didCycleFocusAtRootVisualScope)
+ private IInputElement? GetTabStopCandidateElement(
+ IInputElement? focusedElement,
+ bool isReverse,
+ bool queryOnly,
+ out bool didCycleFocusAtRootVisualScope)
{
didCycleFocusAtRootVisualScope = false;
- var currentFocus = Current;
- IInputElement? newTabStop = null;
- var root = this._contentRoot as IInputElement;
+ IInputElement? newTabStop;
+ var root = _contentRoot;
if (root == null)
return null;
bool internalCycleWorkaround = false;
- if (Current != null)
+ if (focusedElement != null)
{
- internalCycleWorkaround = CanProcessTabStop(isReverse);
+ internalCycleWorkaround = CanProcessTabStop(focusedElement, isReverse);
}
- if (currentFocus == null)
+ if (focusedElement == null)
{
if (!isReverse)
{
@@ -567,7 +572,7 @@ namespace Avalonia.Input
}
else if (!isReverse)
{
- newTabStop = GetNextTabStop();
+ newTabStop = GetNextTabStop(focusedElement);
if (newTabStop == null && (internalCycleWorkaround || queryOnly))
{
@@ -578,7 +583,7 @@ namespace Avalonia.Input
}
else
{
- newTabStop = GetPreviousTabStop();
+ newTabStop = GetPreviousTabStop(focusedElement);
if (newTabStop == null && (internalCycleWorkaround || queryOnly))
{
@@ -590,9 +595,9 @@ namespace Avalonia.Input
return newTabStop;
}
- private IInputElement? GetNextTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false)
+ private IInputElement? GetNextTabStop(IInputElement? currentTabStop, bool ignoreCurrentTabStop = false)
{
- var focused = currentTabStop ?? Current;
+ var focused = currentTabStop;
if (focused == null || _contentRoot == null)
{
return null;
@@ -698,9 +703,9 @@ namespace Avalonia.Input
return newTabStop;
}
- private IInputElement? GetPreviousTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false)
+ private IInputElement? GetPreviousTabStop(IInputElement? currentTabStop, bool ignoreCurrentTabStop = false)
{
- var focused = currentTabStop ?? Current;
+ var focused = currentTabStop;
if (focused == null || _contentRoot == null)
{
return null;
@@ -930,23 +935,23 @@ namespace Avalonia.Input
return int.MaxValue;
}
- private bool CanProcessTabStop(bool isReverse)
+ private bool CanProcessTabStop(IInputElement? focusedElement, bool isReverse)
{
bool isFocusOnFirst = false;
bool isFocusOnLast = false;
bool canProcessTab = true;
- if (IsFocusedElementInPopup())
+ if (IsFocusedElementInPopup(focusedElement))
{
return true;
}
if (isReverse)
{
- isFocusOnFirst = IsFocusOnFirstTabStop();
+ isFocusOnFirst = IsFocusOnFirstTabStop(focusedElement);
}
else
{
- isFocusOnLast = IsFocusOnLastTabStop();
+ isFocusOnLast = IsFocusOnLastTabStop(focusedElement);
}
if (isFocusOnFirst || isFocusOnLast)
@@ -961,7 +966,7 @@ namespace Avalonia.Input
if (edge != null)
{
var edgeParent = GetParentTabStopElement(edge);
- if (edgeParent is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Once && edgeParent == GetParentTabStopElement(Current))
+ if (edgeParent is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Once && edgeParent == GetParentTabStopElement(focusedElement))
{
canProcessTab = false;
}
@@ -975,13 +980,13 @@ namespace Avalonia.Input
{
if (isFocusOnLast || isFocusOnFirst)
{
- if (Current is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Cycle)
+ if (focusedElement is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Cycle)
{
canProcessTab = true;
}
else
{
- var focusedParent = GetParentTabStopElement(Current);
+ var focusedParent = GetParentTabStopElement(focusedElement);
while (focusedParent != null)
{
if (focusedParent is InputElement iE && KeyboardNavigation.GetTabNavigation(iE) == KeyboardNavigationMode.Cycle)
@@ -1041,9 +1046,9 @@ namespace Avalonia.Input
return null;
}
- private bool IsFocusOnLastTabStop()
+ private bool IsFocusOnLastTabStop(IInputElement? focusedElement)
{
- if (Current == null || _contentRoot is not Visual visual)
+ if (focusedElement == null || _contentRoot is not Visual visual)
return false;
var root = visual.VisualRoot as IInputElement;
@@ -1051,12 +1056,12 @@ namespace Avalonia.Input
var lastFocus = GetLastFocusableElement(root, null);
- return lastFocus == Current;
+ return lastFocus == focusedElement;
}
- private bool IsFocusOnFirstTabStop()
+ private bool IsFocusOnFirstTabStop(IInputElement? focusedElement)
{
- if (Current == null || _contentRoot is not Visual visual)
+ if (focusedElement == null || _contentRoot is not Visual visual)
return false;
var root = visual.VisualRoot as IInputElement;
@@ -1064,7 +1069,7 @@ namespace Avalonia.Input
var firstFocus = GetFirstFocusableElement(root, null);
- return firstFocus == Current;
+ return firstFocus == focusedElement;
}
private static IInputElement? GetFirstFocusableElement(IInputElement searchStart, IInputElement? firstFocus = null)
@@ -1091,7 +1096,7 @@ namespace Avalonia.Input
return lastFocus;
}
- private bool IsFocusedElementInPopup() => Current != null && GetRootOfPopupSubTree(Current) != null;
+ private bool IsFocusedElementInPopup(IInputElement? focusedElement) => focusedElement != null && GetRootOfPopupSubTree(focusedElement) != null;
private Visual? GetRootOfPopupSubTree(IInputElement? current)
{
@@ -1099,7 +1104,7 @@ namespace Avalonia.Input
return null;
}
- private bool FindAndSetNextFocus(NavigationDirection direction, XYFocusOptions xYFocusOptions)
+ private bool FindAndSetNextFocus(IInputElement? focusedElement, NavigationDirection direction, XYFocusOptions xYFocusOptions)
{
var focusChanged = false;
if (xYFocusOptions.UpdateManifoldsFromFocusHintRect && xYFocusOptions.FocusHintRectangle != null)
@@ -1107,7 +1112,7 @@ namespace Avalonia.Input
_xyFocus.SetManifoldsFromBounds(xYFocusOptions.FocusHintRectangle ?? default);
}
- if (FindNextFocus(direction, xYFocusOptions, false) is { } nextFocusedElement)
+ if (FindNextFocus(focusedElement, direction, xYFocusOptions, false) is { } nextFocusedElement)
{
focusChanged = nextFocusedElement.Focus();
diff --git a/src/Avalonia.Base/Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs
index 9bd1fb4239..d9e8d36f8b 100644
--- a/src/Avalonia.Base/Input/IFocusManager.cs
+++ b/src/Avalonia.Base/Input/IFocusManager.cs
@@ -41,10 +41,7 @@ namespace Avalonia.Input
/// , ,
/// and .
///
- ///
- /// The options to help identify the next element to receive focus.
- /// They only apply to directional navigation.
- ///
+ /// The options to help identify the next element to receive focus.
/// true if focus moved; otherwise, false.
bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null);
@@ -69,10 +66,7 @@ namespace Avalonia.Input
/// , ,
/// and .
///
- ///
- /// The options to help identify the next element to receive focus.
- /// They only apply to directional navigation.
- ///
+ /// The options to help identify the next element to receive focus.
/// The next element to receive focus, if any.
IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null);
}
diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
index cdb4588fff..a09eccc1a3 100644
--- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
@@ -806,7 +806,43 @@ namespace Avalonia.Base.UnitTests.Input
}
[Fact]
- public void Can_Get_Next_Element_With_Options()
+ public void Can_Get_Next_Element_With_FocusedElement_Option()
+ {
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var target1 = new Button { Focusable = true, Content = "1" };
+ var target2 = new Button { Focusable = true, Content = "2" };
+ var target3 = new Button { Focusable = true, Content = "3" };
+ var target4 = new Button { Focusable = true, Content = "4" };
+ var container = new StackPanel
+ {
+ Children =
+ {
+ target1,
+ target2,
+ target3,
+ target4
+ }
+ };
+ var root = new TestRoot
+ {
+ Child = container
+ };
+
+ var focusManager = FocusManager.GetFocusManager(container);
+ Assert.NotNull(focusManager);
+ Assert.Null(focusManager.GetFocusedElement());
+
+ var next = focusManager.FindNextElement(
+ NavigationDirection.Next,
+ new FindNextElementOptions { FocusedElement = target1 });
+
+ Assert.Equal(next, target2);
+ }
+ }
+
+ [Fact]
+ public void Can_Get_Directional_Next_Element_With_Options()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
@@ -870,6 +906,76 @@ namespace Avalonia.Base.UnitTests.Input
}
}
+ [Fact]
+ public void Can_Get_Directional_Next_Element_With_FocusedElement_Option()
+ {
+ using (UnitTestApplication.Start(TestServices.RealFocus))
+ {
+ var target1 = new Button { Focusable = true, Content = "1" };
+ var target2 = new Button { Focusable = true, Content = "2" };
+ var target3 = new Button { Focusable = true, Content = "3" };
+ var target4 = new Button { Focusable = true, Content = "4" };
+ var target5 = new Button { Focusable = true, Content = "5" };
+ var searchStack = new StackPanel()
+ {
+ Children =
+ {
+ target3,
+ target4
+ }
+ };
+ var container = new StackPanel
+ {
+ Orientation = Avalonia.Layout.Orientation.Horizontal,
+ Children =
+ {
+ target1,
+ target2,
+ searchStack,
+ target5
+ }
+ };
+ var root = new TestRoot
+ {
+ Child = container
+ };
+
+ root.InvalidateMeasure();
+ root.ExecuteInitialLayoutPass();
+
+ var focusManager = FocusManager.GetFocusManager(container);
+ Assert.NotNull(focusManager);
+ Assert.Null(focusManager.GetFocusedElement());
+
+ // Search root is right of the specified focused element, should return the first focusable element in the search root
+ var next = focusManager.FindNextElement(NavigationDirection.Right, new FindNextElementOptions
+ {
+ SearchRoot = searchStack,
+ FocusedElement = target1
+ });
+
+ Assert.Equal(next, target3);
+
+ // Search root is left of the specified focused element, should return the first focusable element in the search root
+ next = focusManager.FindNextElement(NavigationDirection.Left, new FindNextElementOptions
+ {
+ SearchRoot = searchStack,
+ FocusedElement = target5
+ });
+
+ Assert.Equal(next, target3);
+
+ // Search root isn't to the right of the specified focused element, should return null
+ next = focusManager.FindNextElement(NavigationDirection.Right, new FindNextElementOptions
+ {
+ SearchRoot = searchStack,
+ FocusedElement = target5
+ });
+
+ Assert.Null(next);
+ }
+ }
+
[Fact]
public void Focus_Should_Move_According_To_Direction()
{