diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs
index 474b212a21..1432092ba1 100644
--- a/src/Avalonia.Input/FocusManager.cs
+++ b/src/Avalonia.Input/FocusManager.cs
@@ -92,6 +92,17 @@ namespace Avalonia.Input
}
}
+ public IInputElement? GetFocusedElement(IInputElement e)
+ {
+ if (e is IFocusScope scope)
+ {
+ _focusScopes.TryGetValue(scope, out var result);
+ return result;
+ }
+
+ return null;
+ }
+
///
/// Sets the currently focused element in the specified scope.
///
@@ -151,6 +162,8 @@ namespace Avalonia.Input
Focus(e);
}
+ public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope;
+
///
/// Checks if the specified element can be focused.
///
diff --git a/src/Avalonia.Input/KeyboardNavigation.cs b/src/Avalonia.Input/KeyboardNavigation.cs
index ffccd8f7a0..a25aed6811 100644
--- a/src/Avalonia.Input/KeyboardNavigation.cs
+++ b/src/Avalonia.Input/KeyboardNavigation.cs
@@ -11,7 +11,8 @@ namespace Avalonia.Input
public static readonly AttachedProperty TabIndexProperty =
AvaloniaProperty.RegisterAttached(
"TabIndex",
- typeof(KeyboardNavigation));
+ typeof(KeyboardNavigation),
+ int.MaxValue);
///
/// Defines the TabNavigation attached property.
diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs
index dbefe63789..53c9b008ff 100644
--- a/src/Avalonia.Input/KeyboardNavigationHandler.cs
+++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs
@@ -64,7 +64,13 @@ namespace Avalonia.Input
}
else if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
{
- return TabNavigation.GetNextInTabOrder((IInputElement)customHandler, direction, true);
+ var e = (IInputElement)customHandler;
+ return direction switch
+ {
+ NavigationDirection.Next => TabNavigation.GetNextTab(e, false),
+ NavigationDirection.Previous => TabNavigation.GetPrevTab(e, null, false),
+ _ => throw new NotSupportedException(),
+ };
}
else
{
@@ -73,14 +79,12 @@ namespace Avalonia.Input
}
}
- if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
+ return direction switch
{
- return TabNavigation.GetNextInTabOrder(element, direction);
- }
- else
- {
- throw new NotSupportedException();
- }
+ NavigationDirection.Next => TabNavigation.GetNextTab(element, false),
+ NavigationDirection.Previous => TabNavigation.GetPrevTab(element, null, false),
+ _ => throw new NotSupportedException(),
+ };
}
///
diff --git a/src/Avalonia.Input/Navigation/TabNavigation.cs b/src/Avalonia.Input/Navigation/TabNavigation.cs
index 6f6d68940b..d1862c2fa5 100644
--- a/src/Avalonia.Input/Navigation/TabNavigation.cs
+++ b/src/Avalonia.Input/Navigation/TabNavigation.cs
@@ -10,277 +10,634 @@ namespace Avalonia.Input.Navigation
///
internal static class TabNavigation
{
- ///
- /// Gets the next control in the specified tab direction.
- ///
- /// The element.
- /// The tab direction. Must be Next or Previous.
- ///
- /// If true will not descend into to find next control.
- ///
- ///
- /// The next element in the specified direction, or null if
- /// was the last in the requested direction.
- ///
- public static IInputElement? GetNextInTabOrder(
- IInputElement element,
- NavigationDirection direction,
- bool outsideElement = false)
+ public static IInputElement? GetNextTab(IInputElement e, bool goDownOnly)
{
- element = element ?? throw new ArgumentNullException(nameof(element));
-
- if (direction != NavigationDirection.Next && direction != NavigationDirection.Previous)
- {
- throw new ArgumentException("Invalid direction: must be Next or Previous.");
- }
+ return GetNextTab(e, GetGroupParent(e), goDownOnly);
+ }
- var container = element.GetVisualParent();
+ public static IInputElement? GetNextTab(IInputElement? e, IInputElement container, bool goDownOnly)
+ {
+ var tabbingType = GetKeyNavigationMode(container);
- if (container != null)
+ if (e == null)
{
- var mode = KeyboardNavigation.GetTabNavigation((InputElement)container);
+ if (IsTabStop(container))
+ return container;
- switch (mode)
+ // Using ActiveElement if set
+ var activeElement = GetActiveElement(container);
+ if (activeElement != null)
+ return GetNextTab(null, activeElement, true);
+ }
+ else
+ {
+ if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
{
- case KeyboardNavigationMode.Continue:
- return GetNextInContainer(element, container, direction, outsideElement) ??
- GetFirstInNextContainer(element, element, direction);
- case KeyboardNavigationMode.Cycle:
- return GetNextInContainer(element, container, direction, outsideElement) ??
- GetFocusableDescendant(container, direction);
- case KeyboardNavigationMode.Contained:
- return GetNextInContainer(element, container, direction, outsideElement);
- default:
- return GetFirstInNextContainer(element, container, direction);
+ if (container != e)
+ {
+ if (goDownOnly)
+ return null;
+ var parentContainer = GetGroupParent(container);
+ return GetNextTab(container, parentContainer, goDownOnly);
+ }
}
}
- else
+
+ // All groups
+ IInputElement? loopStartElement = null;
+ var nextTabElement = e;
+ var currentTabbingType = tabbingType;
+
+ // Search down inside the container
+ while ((nextTabElement = GetNextTabInGroup(nextTabElement, container, currentTabbingType)) != null)
{
- return GetFocusableDescendants(element, direction).FirstOrDefault();
+ // Avoid the endless loop here for Cycle groups
+ if (loopStartElement == nextTabElement)
+ break;
+ if (loopStartElement == null)
+ loopStartElement = nextTabElement;
+
+ var firstTabElementInside = GetNextTab(null, nextTabElement, true);
+ if (firstTabElementInside != null)
+ return firstTabElementInside;
+
+ // If we want to continue searching inside the Once groups, we should change the navigation mode
+ if (currentTabbingType == KeyboardNavigationMode.Once)
+ currentTabbingType = KeyboardNavigationMode.Contained;
}
- }
- ///
- /// Gets the first or last focusable descendant of the specified element.
- ///
- /// The element.
- /// The direction to search.
- /// The element or null if not found.##
- private static IInputElement GetFocusableDescendant(IInputElement container, NavigationDirection direction)
- {
- return direction == NavigationDirection.Next ?
- GetFocusableDescendants(container, direction).FirstOrDefault() :
- GetFocusableDescendants(container, direction).LastOrDefault();
+ // If there is no next element in the group (nextTabElement == null)
+
+ // Search up in the tree if allowed
+ // consider: Use original tabbingType instead of currentTabbingType
+ if (!goDownOnly && currentTabbingType != KeyboardNavigationMode.Contained && GetParent(container) != null)
+ {
+ return GetNextTab(container, GetGroupParent(container), false);
+ }
+
+ return null;
}
- ///
- /// Gets the focusable descendants of the specified element.
- ///
- /// The element.
- /// The tab direction. Must be Next or Previous.
- /// The element's focusable descendants.
- private static IEnumerable GetFocusableDescendants(IInputElement element,
- NavigationDirection direction)
+ public static IInputElement? GetPrevTab(IInputElement? e, IInputElement? container, bool goDownOnly)
{
- var mode = KeyboardNavigation.GetTabNavigation((InputElement)element);
+ if (e is null && container is null)
+ throw new InvalidOperationException("Either 'e' or 'container' must be non-null.");
- if (mode == KeyboardNavigationMode.None)
- {
- yield break;
- }
+ if (container is null)
+ container = GetGroupParent(e!);
- var children = element.GetVisualChildren().OfType();
+ KeyboardNavigationMode tabbingType = GetKeyNavigationMode(container);
- if (mode == KeyboardNavigationMode.Once)
+ if (e == null)
{
- var active = KeyboardNavigation.GetTabOnceActiveElement((InputElement)element);
-
- if (active != null)
+ // Using ActiveElement if set
+ var activeElement = GetActiveElement(container);
+ if (activeElement != null)
+ return GetPrevTab(null, activeElement, true);
+ else
{
- yield return active;
- yield break;
+ // If we Shift+Tab on a container with KeyboardNavigationMode=Once, and ActiveElement is null
+ // then we want to go to the first item (not last) within the container
+ if (tabbingType == KeyboardNavigationMode.Once)
+ {
+ var firstTabElement = GetNextTabInGroup(null, container, tabbingType);
+ if (firstTabElement == null)
+ {
+ if (IsTabStop(container))
+ return container;
+ if (goDownOnly)
+ return null;
+
+ return GetPrevTab(container, null, false);
+ }
+ else
+ {
+ return GetPrevTab(null, firstTabElement, true);
+ }
+ }
}
- else
+ }
+ else
+ {
+ if (tabbingType == KeyboardNavigationMode.Once || tabbingType == KeyboardNavigationMode.None)
{
- children = children.Take(1);
+ if (goDownOnly || container == e)
+ return null;
+
+ // FocusedElement should not be e otherwise we will delegate focus to the same element
+ if (IsTabStop(container))
+ return container;
+
+ return GetPrevTab(container, null, false);
}
}
- foreach (var child in children)
+ // All groups (except Once) - continue
+ IInputElement? loopStartElement = null;
+ IInputElement? nextTabElement = e;
+
+ // Look for element with the same TabIndex before the current element
+ while ((nextTabElement = GetPrevTabInGroup(nextTabElement, container, tabbingType)) != null)
{
- var customNext = GetCustomNext(child, direction);
+ // At this point nextTabElement is TabStop or TabGroup
+ // In case it is a TabStop only return the element
+ if (IsTabStop(nextTabElement) && !IsGroup(nextTabElement))
+ return nextTabElement;
+
+ // Avoid the endless loop here
+ if (loopStartElement == nextTabElement)
+ break;
+ if (loopStartElement == null)
+ loopStartElement = nextTabElement;
+
+ // At this point nextTabElement is TabGroup
+ var lastTabElementInside = GetPrevTab(null, nextTabElement, true);
+ if (lastTabElementInside != null)
+ return lastTabElementInside;
+ }
- if (customNext.handled)
- {
- yield return customNext.next!;
- }
- else
+ if (tabbingType == KeyboardNavigationMode.Contained)
+ return null;
+
+ if (e != container && IsTabStop(container))
+ return container;
+
+ // If end of the subtree is reached or there no other elements above
+ if (!goDownOnly && GetParent(container) != null)
+ {
+ return GetPrevTab(container, null, false);
+ }
+
+ return null;
+ }
+
+ private static IInputElement? FocusedElement(IInputElement e)
+ {
+ var iie = e;
+ // Focus delegation is enabled only if keyboard focus is outside the container
+ if (iie != null && !iie.IsKeyboardFocusWithin)
+ {
+ var focusedElement = (FocusManager.Instance as FocusManager)?.GetFocusedElement(e);
+ if (focusedElement != null)
{
- if (child.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement)child))
+ if (!IsFocusScope(e))
{
- yield return child;
+ // Verify if focusedElement is a visual descendant of e
+ if (focusedElement is IVisual visualFocusedElement &&
+ visualFocusedElement != e &&
+ e.IsVisualAncestorOf(visualFocusedElement))
+ {
+ return focusedElement;
+ }
}
+ }
+ }
- if (child.CanFocusDescendants())
+ return null;
+ }
+
+ private static IInputElement? GetFirstChild(IInputElement e)
+ {
+ // If the element has a FocusedElement it should be its first child
+ if (FocusedElement(e) is IInputElement focusedElement)
+ return focusedElement;
+
+ // Return the first visible element.
+ var uiElement = e as InputElement;
+
+ if (uiElement is null || uiElement.IsVisible)
+ {
+ if (e is IVisual elementAsVisual)
+ {
+ var children = elementAsVisual.VisualChildren;
+ var count = children.Count;
+
+ for (int i = 0; i < count; i++)
{
- foreach (var descendant in GetFocusableDescendants(child, direction))
+ if (children[i] is InputElement ie)
{
- if (KeyboardNavigation.GetIsTabStop((InputElement)descendant))
+ if (ie.IsVisible)
+ return ie;
+ else
{
- yield return descendant;
+ var firstChild = GetFirstChild(ie);
+ if (firstChild != null)
+ return firstChild;
}
}
}
}
}
+
+ return null;
}
- ///
- /// Gets the next item that should be focused in the specified container.
- ///
- /// The starting element/
- /// The container.
- /// The direction.
- ///
- /// If true will not descend into to find next control.
- ///
- /// The next element, or null if the element is the last.
- private static IInputElement? GetNextInContainer(
- IInputElement element,
- IInputElement container,
- NavigationDirection direction,
- bool outsideElement)
+ private static IInputElement? GetLastChild(IInputElement e)
{
- IInputElement? e = element;
-
- if (direction == NavigationDirection.Next && !outsideElement)
- {
- var descendant = GetFocusableDescendants(element, direction).FirstOrDefault();
+ // If the element has a FocusedElement it should be its last child
+ if (FocusedElement(e) is IInputElement focusedElement)
+ return focusedElement;
- if (descendant != null)
- {
- return descendant;
- }
- }
+ // Return the last visible element.
+ var uiElement = e as InputElement;
- if (container != null)
+ if (uiElement == null || uiElement.IsVisible)
{
- var navigable = container as INavigableContainer;
+ var elementAsVisual = e as IVisual;
- // TODO: Do a spatial search here if the container doesn't implement
- // INavigableContainer.
- if (navigable != null)
+ if (elementAsVisual != null)
{
- while (e != null)
- {
- e = navigable.GetControl(direction, e, false);
+ var children = elementAsVisual.VisualChildren;
+ var count = children.Count;
- if (e != null &&
- e.CanFocus() &&
- KeyboardNavigation.GetIsTabStop((InputElement)e))
+ for (int i = count - 1; i >= 0; i--)
+ {
+ if (children[i] is InputElement ie)
{
- break;
+ if (ie.IsVisible)
+ return ie;
+ else
+ {
+ var lastChild = GetLastChild(ie);
+ if (lastChild != null)
+ return lastChild;
+ }
}
}
}
- else
+ }
+
+ return null;
+ }
+
+ private static IInputElement? GetFirstTabInGroup(IInputElement container)
+ {
+ IInputElement? firstTabElement = null;
+ int minIndexFirstTab = int.MinValue;
+
+ var currElement = container;
+ while ((currElement = GetNextInTree(currElement, container)) != null)
+ {
+ if (IsTabStopOrGroup(currElement))
{
- // TODO: Do a spatial search here if the container doesn't implement
- // INavigableContainer.
- e = null;
+ int currPriority = KeyboardNavigation.GetTabIndex(currElement);
+
+ if (currPriority < minIndexFirstTab || firstTabElement == null)
+ {
+ minIndexFirstTab = currPriority;
+ firstTabElement = currElement;
+ }
}
+ }
+ return firstTabElement;
+ }
+
+ private static IInputElement? GetLastInTree(IInputElement container)
+ {
+ IInputElement? result;
+ IInputElement? c = container;
+
+ do
+ {
+ result = c;
+ c = GetLastChild(c);
+ } while (c != null && !IsGroup(c));
+
+ if (c != null)
+ return c;
+
+ return result;
+ }
- if (e != null && direction == NavigationDirection.Previous)
+ private static IInputElement? GetLastTabInGroup(IInputElement container)
+ {
+ IInputElement? lastTabElement = null;
+ int maxIndexFirstTab = int.MaxValue;
+ var currElement = GetLastInTree(container);
+ while (currElement != null && currElement != container)
+ {
+ if (IsTabStopOrGroup(currElement))
{
- var descendant = GetFocusableDescendants(e, direction).LastOrDefault();
+ int currPriority = KeyboardNavigation.GetTabIndex(currElement);
- if (descendant != null)
+ if (currPriority > maxIndexFirstTab || lastTabElement == null)
{
- return descendant;
+ maxIndexFirstTab = currPriority;
+ lastTabElement = currElement;
}
}
+ currElement = GetPreviousInTree(currElement, container);
+ }
+ return lastTabElement;
+ }
+
+ private static IInputElement? GetNextInTree(IInputElement e, IInputElement container)
+ {
+ IInputElement? result = null;
- return e;
+ if (e == container || !IsGroup(e))
+ result = GetFirstChild(e);
+
+ if (result != null || e == container)
+ return result;
+
+ IInputElement? parent = e;
+ do
+ {
+ var sibling = GetNextSibling(parent);
+ if (sibling != null)
+ return sibling;
+
+ parent = GetParent(parent);
+ } while (parent != null && parent != container);
+
+ return null;
+ }
+
+ private static IInputElement? GetNextSibling(IInputElement e)
+ {
+ if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual)
+ {
+ var children = parentAsVisual.VisualChildren;
+ var count = children.Count;
+ var i = 0;
+
+ //go till itself
+ for (; i < count; i++)
+ {
+ var vchild = children[i];
+ if (vchild == elementAsVisual)
+ break;
+ }
+ i++;
+ //search ahead
+ for (; i < count; i++)
+ {
+ var visual = children[i];
+ if (visual is IInputElement ie)
+ return ie;
+ }
}
return null;
}
- ///
- /// Gets the first item that should be focused in the next container.
- ///
- /// The element being navigated away from.
- /// The container.
- /// The direction of the search.
- /// The first element, or null if there are no more elements.
- private static IInputElement? GetFirstInNextContainer(
- IInputElement element,
- IInputElement container,
- NavigationDirection direction)
+ private static IInputElement? GetNextTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType)
{
- var parent = container.GetVisualParent();
- IInputElement? next = null;
+ // None groups: Tab navigation is not supported
+ if (tabbingType == KeyboardNavigationMode.None)
+ return null;
+
+ // e == null or e == container -> return the first TabStopOrGroup
+ if (e == null || e == container)
+ {
+ return GetFirstTabInGroup(container);
+ }
+
+ if (tabbingType == KeyboardNavigationMode.Once)
+ return null;
+
+ var nextTabElement = GetNextTabWithSameIndex(e, container);
+ if (nextTabElement != null)
+ return nextTabElement;
+
+ return GetNextTabWithNextIndex(e, container, tabbingType);
+ }
- if (parent != null)
+ private static IInputElement? GetNextTabWithSameIndex(IInputElement e, IInputElement container)
+ {
+ var elementTabPriority = KeyboardNavigation.GetTabIndex(e);
+ var currElement = e;
+ while ((currElement = GetNextInTree(currElement, container)) != null)
{
- if (direction == NavigationDirection.Previous &&
- parent.CanFocus() &&
- KeyboardNavigation.GetIsTabStop((InputElement) parent))
+ if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority)
{
- return parent;
+ return currElement;
}
+ }
- var allSiblings = parent.GetVisualChildren()
- .OfType()
- .Where(FocusExtensions.CanFocusDescendants);
- var siblings = direction == NavigationDirection.Next ?
- allSiblings.SkipWhile(x => x != container).Skip(1) :
- allSiblings.TakeWhile(x => x != container).Reverse();
+ return null;
+ }
- foreach (var sibling in siblings)
+ private static IInputElement? GetNextTabWithNextIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType)
+ {
+ // Find the next min index in the tree
+ // min (index>currentTabIndex)
+ IInputElement? nextTabElement = null;
+ IInputElement? firstTabElement = null;
+ int minIndexFirstTab = int.MinValue;
+ int minIndex = int.MinValue;
+ int elementTabPriority = KeyboardNavigation.GetTabIndex(e);
+
+ IInputElement? currElement = container;
+ while ((currElement = GetNextInTree(currElement, container)) != null)
+ {
+ if (IsTabStopOrGroup(currElement))
{
- var customNext = GetCustomNext(sibling, direction);
- if (customNext.handled)
+ int currPriority = KeyboardNavigation.GetTabIndex(currElement);
+ if (currPriority > elementTabPriority)
{
- return customNext.next;
+ if (currPriority < minIndex || nextTabElement == null)
+ {
+ minIndex = currPriority;
+ nextTabElement = currElement;
+ }
}
- if (sibling.CanFocus() && KeyboardNavigation.GetIsTabStop((InputElement) sibling))
+ if (currPriority < minIndexFirstTab || firstTabElement == null)
{
- return sibling;
+ minIndexFirstTab = currPriority;
+ firstTabElement = currElement;
}
+ }
+ }
+
+ // Cycle groups: if not found - return first element
+ if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null)
+ nextTabElement = firstTabElement;
- next = direction == NavigationDirection.Next ?
- GetFocusableDescendants(sibling, direction).FirstOrDefault() :
- GetFocusableDescendants(sibling, direction).LastOrDefault();
+ return nextTabElement;
+ }
- if (next != null)
+ private static IInputElement? GetPrevTabInGroup(IInputElement? e, IInputElement container, KeyboardNavigationMode tabbingType)
+ {
+ // None groups: Tab navigation is not supported
+ if (tabbingType == KeyboardNavigationMode.None)
+ return null;
+
+ // Search the last index inside the group
+ if (e == null)
+ {
+ return GetLastTabInGroup(container);
+ }
+
+ if (tabbingType == KeyboardNavigationMode.Once)
+ return null;
+
+ if (e == container)
+ return null;
+
+ var nextTabElement = GetPrevTabWithSameIndex(e, container);
+ if (nextTabElement != null)
+ return nextTabElement;
+
+ return GetPrevTabWithPrevIndex(e, container, tabbingType);
+ }
+
+ private static IInputElement? GetPrevTabWithSameIndex(IInputElement e, IInputElement container)
+ {
+ int elementTabPriority = KeyboardNavigation.GetTabIndex(e);
+ var currElement = GetPreviousInTree(e, container);
+ while (currElement != null)
+ {
+ if (IsTabStopOrGroup(currElement) && KeyboardNavigation.GetTabIndex(currElement) == elementTabPriority && currElement != container)
+ {
+ return currElement;
+ }
+ currElement = GetPreviousInTree(currElement, container);
+ }
+ return null;
+ }
+
+ private static IInputElement? GetPrevTabWithPrevIndex(IInputElement e, IInputElement container, KeyboardNavigationMode tabbingType)
+ {
+ // Find the next max index in the tree
+ // max (index maxIndex || nextTabElement == null)
+ {
+ maxIndex = currPriority;
+ nextTabElement = currElement;
+ }
+ }
+
+ if (currPriority > maxIndexFirstTab || lastTabElement == null)
+ {
+ maxIndexFirstTab = currPriority;
+ lastTabElement = currElement;
}
}
- next = GetFirstInNextContainer(element, parent, direction);
+ currElement = GetPreviousInTree(currElement, container);
+ }
+
+ // Cycle groups: if not found - return first element
+ if (tabbingType == KeyboardNavigationMode.Cycle && nextTabElement == null)
+ nextTabElement = lastTabElement;
+
+ return nextTabElement;
+ }
+
+ private static IInputElement? GetPreviousInTree(IInputElement e, IInputElement container)
+ {
+ if (e == container)
+ return null;
+
+ var result = GetPreviousSibling(e);
+
+ if (result != null)
+ {
+ if (IsGroup(result))
+ return result;
+ else
+ return GetLastInTree(result);
}
else
+ return GetParent(e);
+ }
+
+ private static IInputElement? GetPreviousSibling(IInputElement e)
+ {
+ if (GetParent(e) is IVisual parentAsVisual && e is IVisual elementAsVisual)
{
- next = direction == NavigationDirection.Next ?
- GetFocusableDescendants(container, direction).FirstOrDefault() :
- GetFocusableDescendants(container, direction).LastOrDefault();
+ var children = parentAsVisual.VisualChildren;
+ var count = children.Count;
+ IInputElement? prev = null;
+
+ for (int i = 0; i < count; i++)
+ {
+ var vchild = children[i];
+ if (vchild == elementAsVisual)
+ break;
+ if (vchild.IsVisible == true && vchild is IInputElement ie)
+ prev = ie;
+ }
+ return prev;
}
+ return null;
+ }
- return next;
+ private static IInputElement? GetActiveElement(IInputElement e)
+ {
+ return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabOnceActiveElementProperty);
}
- private static (bool handled, IInputElement? next) GetCustomNext(IInputElement element,
- NavigationDirection direction)
+ private static IInputElement GetGroupParent(IInputElement e) => GetGroupParent(e, false);
+
+ private static IInputElement GetGroupParent(IInputElement element, bool includeCurrent)
{
- if (element is ICustomKeyboardNavigation custom)
+ var result = element; // Keep the last non null element
+ var e = element;
+
+ // If we don't want to include the current element,
+ // start at the parent of the element. If the element
+ // is the root, then just return it as the group parent.
+ if (!includeCurrent)
+ {
+ result = e;
+ e = GetParent(e);
+ if (e == null)
+ return result;
+ }
+
+ while (e != null)
{
- return custom.GetNext(element, direction);
+ if (IsGroup(e))
+ return e;
+
+ result = e;
+ e = GetParent(e);
}
- return (false, null);
+ return result;
+ }
+
+ private static IInputElement? GetParent(IInputElement e)
+ {
+ // For Visual - go up the visual parent chain until we find Visual.
+ if (e is IVisual v)
+ return v.FindAncestorOfType();
+
+ // This will need to be implemented when we have non-visual input elements.
+ throw new NotSupportedException();
+ }
+
+ private static KeyboardNavigationMode GetKeyNavigationMode(IInputElement e)
+ {
+ return ((IAvaloniaObject)e).GetValue(KeyboardNavigation.TabNavigationProperty);
+ }
+ private static bool IsFocusScope(IInputElement e) => FocusManager.GetIsFocusScope(e) || GetParent(e) == null;
+ private static bool IsGroup(IInputElement e) => GetKeyNavigationMode(e) != KeyboardNavigationMode.Continue;
+
+ private static bool IsTabStop(IInputElement e)
+ {
+ if (e is InputElement ie)
+ return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEnabled;
+ return false;
}
+
+ private static bool IsTabStopOrGroup(IInputElement e) => IsTabStop(e) || IsGroup(e);
}
}
diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
index 1efbbed2e8..f471ae4029 100644
--- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
+++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs
@@ -13,6 +13,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -49,6 +50,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -85,6 +87,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -122,6 +125,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -165,6 +169,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -193,6 +198,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -222,6 +228,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -263,6 +270,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(next = new Button { Name = "Button1" }),
@@ -282,6 +290,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -324,6 +333,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -361,6 +371,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -398,6 +409,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -434,6 +446,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -471,6 +484,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -509,6 +523,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(container = new StackPanel
@@ -548,6 +563,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -586,6 +602,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(container = new StackPanel
@@ -625,6 +642,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -661,6 +679,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -697,6 +716,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -725,6 +745,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -767,6 +788,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -828,6 +850,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -865,6 +888,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -902,6 +926,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -938,6 +963,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -975,6 +1001,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel
@@ -1013,6 +1040,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
(container = new StackPanel
@@ -1052,6 +1080,7 @@ namespace Avalonia.Input.UnitTests
var top = new StackPanel
{
+ [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle,
Children =
{
new StackPanel