瀏覽代碼

Focus Traversal Api (#18647)

* add focus traversal

* add control focus search overrides

* fix whitespace

* rebased and updated api

* update tests, make focusmanager constructor public

* fix tests

* fix whitespace issue

* addressed comment

* add more tests

* remove redundant private focus  state. fix tests

* Update FocusManager.cs

* fix constructors

---------

Co-authored-by: Julien Lebosquain <[email protected]>
Emmanuel Hansen 1 月之前
父節點
當前提交
22eb8e6ec3
共有 29 個文件被更改,包括 1434 次插入75 次删除
  1. 17 0
      src/Avalonia.Base/Input/FindNextElementOptions.cs
  2. 95 0
      src/Avalonia.Base/Input/FocusHelpers.cs
  3. 887 12
      src/Avalonia.Base/Input/FocusManager.cs
  4. 159 13
      src/Avalonia.Base/Input/InputElement.cs
  5. 9 8
      src/Avalonia.Base/Input/Navigation/TabNavigation.cs
  6. 1 1
      src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs
  7. 8 8
      src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs
  8. 0 2
      src/Avalonia.Controls/Application.cs
  9. 2 1
      src/Avalonia.Controls/TopLevel.cs
  10. 251 0
      tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
  11. 0 1
      tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs
  12. 0 1
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  13. 1 2
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  14. 0 1
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  15. 0 1
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  16. 0 1
      tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs
  17. 0 1
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  18. 0 2
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  19. 0 1
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  20. 0 1
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  21. 0 1
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
  22. 0 2
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  23. 0 1
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  24. 0 1
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  25. 0 1
      tests/Avalonia.LeakTests/ControlTests.cs
  26. 1 1
      tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs
  27. 2 1
      tests/Avalonia.UnitTests/TestRoot.cs
  28. 1 9
      tests/Avalonia.UnitTests/TestServices.cs
  29. 0 1
      tests/Avalonia.UnitTests/UnitTestApplication.cs

+ 17 - 0
src/Avalonia.Base/Input/FindNextElementOptions.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Avalonia.Input
+{
+    public sealed class FindNextElementOptions
+    {
+        public InputElement? SearchRoot { get; init; }
+        public Rect ExclusionRect { get; init; }
+        public Rect? FocusHintRectangle { get; init; }
+        public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; }
+        public bool IgnoreOcclusivity { get; init; }
+    }
+}

+ 95 - 0
src/Avalonia.Base/Input/FocusHelpers.cs

@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.VisualTree;
+
+namespace Avalonia.Input
+{
+    internal static class FocusHelpers
+    {
+        public static IEnumerable<IInputElement> GetInputElementChildren(AvaloniaObject? parent)
+        {
+            // TODO: add control overrides to return custom focus list from control
+            if (parent is Visual visual)
+            {
+                return visual.VisualChildren.OfType<IInputElement>();
+            }
+
+            return Array.Empty<IInputElement>();
+        }
+
+        public static bool CanHaveFocusableChildren(AvaloniaObject? parent)
+        {
+            if (parent == null)
+                return false;
+
+            var children = GetInputElementChildren(parent);
+
+            bool hasFocusChildren = true;
+
+            foreach (var child in children)
+            {
+                if (IsVisible(child))
+                {
+                    if (child.Focusable)
+                    {
+                        hasFocusChildren = true;
+                    }
+                    else if (CanHaveFocusableChildren(child as AvaloniaObject))
+                    {
+                        hasFocusChildren = true;
+                    }
+                }
+
+                if (hasFocusChildren)
+                    break;
+            }
+
+            return hasFocusChildren;
+        }
+
+        public static IInputElement? GetFocusParent(IInputElement? inputElement)
+        {
+            if (inputElement == null)
+                return null;
+
+            if (inputElement is Visual visual)
+            {
+                var rootVisual = visual.VisualRoot;
+                if (inputElement != rootVisual)
+                    return visual.Parent as IInputElement;
+            }
+
+            return null;
+        }
+
+        public static bool IsPotentialTabStop(IInputElement? element)
+        {
+            if (element is InputElement inputElement)
+                return inputElement.IsTabStop;
+
+            return false;
+        }
+
+        internal static bool IsVisible(IInputElement? element)
+        {
+            if(element is Visual visual)
+                return visual.IsEffectivelyVisible;
+
+            return false;
+        }
+
+        internal static bool IsFocusable(IInputElement? element)
+        {
+            return element?.Focusable ?? false;
+        }
+
+        internal static bool CanHaveChildren(IInputElement? element)
+        {
+            // We don't currently have a flag to indicate a visual can have children, so we just return whether the element is a visual
+            return element is Visual;
+        }
+    }
+}

+ 887 - 12
src/Avalonia.Base/Input/FocusManager.cs

@@ -1,6 +1,10 @@
 using System;
+using System.Diagnostics;
+using System.Linq;
+using Avalonia.Input.Navigation;
 using Avalonia.Interactivity;
 using Avalonia.Metadata;
+using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Input
@@ -34,8 +38,22 @@ namespace Avalonia.Input
                 RoutingStrategies.Tunnel);
         }
 
+        public FocusManager()
+        {
+            _contentRoot = null;
+        }
+
+        public FocusManager(IInputElement contentRoot)
+        {
+            _contentRoot = contentRoot;
+        }
+
         private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement;
 
+        private XYFocus _xyFocus = new();
+        private XYFocusOptions _xYFocusOptions = new XYFocusOptions();
+        private IInputElement? _contentRoot;
+
         /// <summary>
         /// Gets the currently focused <see cref="IInputElement"/>.
         /// </summary>
@@ -48,7 +66,7 @@ namespace Avalonia.Input
         /// <param name="method">The method by which focus was changed.</param>
         /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
         public bool Focus(
-            IInputElement? control, 
+            IInputElement? control,
             NavigationMethod method = NavigationMethod.Unspecified,
             KeyModifiers keyModifiers = KeyModifiers.None)
         {
@@ -69,7 +87,7 @@ namespace Avalonia.Input
                 keyboardDevice.SetFocusedElement(control, method, keyModifiers);
                 return true;
             }
-            else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore && 
+            else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
                 restore != Current &&
                 Focus(restore))
             {
@@ -144,21 +162,31 @@ namespace Avalonia.Input
             // Element might not be a visual, and not attached to the root.
             // But IFocusManager is always expected to be a FocusManager. 
             return (FocusManager?)((element as Visual)?.VisualRoot as IInputRoot)?.FocusManager
-                   // In our unit tests some elements might not have a root. Remove when we migrate to headless tests.
+                // In our unit tests some elements might not have a root. Remove when we migrate to headless tests.
                 ?? (FocusManager?)AvaloniaLocator.Current.GetService<IFocusManager>();
         }
 
-        internal bool TryMoveFocus(NavigationDirection direction)
+        /// <summary>
+        /// Attempts to change focus from the element with focus to the next focusable element in the specified direction.
+        /// </summary>
+        /// <param name="direction">The direction to traverse (in tab order).</param>
+        /// <returns>true if focus moved; otherwise, false.</returns>
+        public bool TryMoveFocus(NavigationDirection direction)
         {
-            if (GetFocusedElement() is {} focusedElement
-                && KeyboardNavigationHandler.GetNext(focusedElement, direction) is {} newElement)
-            {
-                return newElement.Focus();
-            }
+            return FindAndSetNextFocus(direction, _xYFocusOptions);
+        }
 
-            return false;
+        /// <summary>
+        /// Attempts to change focus from the element with focus to the next focusable element in the specified direction, using the specified navigation options.
+        /// </summary>
+        /// <param name="direction">The direction to traverse (in tab order).</param>
+        /// <param name="options">The options to help identify the next element to receive focus with keyboard/controller/remote navigation.</param>
+        /// <returns>true if focus moved; otherwise, false.</returns>
+        public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions options)
+        {
+            return FindAndSetNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
         }
-        
+
         /// <summary>
         /// Checks if the specified element can be focused.
         /// </summary>
@@ -233,7 +261,7 @@ namespace Avalonia.Input
 
                         break;
                     }
-                    
+
                     element = element.VisualParent;
                 }
             }
@@ -245,5 +273,852 @@ namespace Avalonia.Input
                 return v.IsAttachedToVisualTree && e.IsEffectivelyVisible;
             return true;
         }
+
+        /// <summary>
+        /// Retrieves the first element that can receive focus.
+        /// </summary>
+        /// <returns>The first focusable element.</returns>
+        public IInputElement? FindFirstFocusableElement()
+        {
+            var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
+            if (root == null)
+                return null;
+            return GetFirstFocusableElementFromRoot(false);
+        }
+
+        /// <summary>
+        /// Retrieves the first element that can receive focus based on the specified scope.
+        /// </summary>
+        /// <param name="searchScope">The root element from which to search.</param>
+        /// <returns>The first focusable element.</returns>
+        public static IInputElement? FindFirstFocusableElement(IInputElement searchScope)
+        {
+            return GetFirstFocusableElement(searchScope);
+        }
+
+        /// <summary>
+        /// Retrieves the last element that can receive focus.
+        /// </summary>
+        /// <returns>The last focusable element.</returns>
+        public IInputElement? FindLastFocusableElement()
+        {
+            var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
+            if (root == null)
+                return null;
+            return GetFirstFocusableElementFromRoot(true);
+        }
+
+        /// <summary>
+        /// Retrieves the last element that can receive focus based on the specified scope.
+        /// </summary>
+        /// <param name="searchScope">The root element from which to search.</param>
+        /// <returns>The last focusable object.</returns>
+        public static IInputElement? FindLastFocusableElement(IInputElement searchScope)
+        {
+            return GetFocusManager(searchScope)?.GetLastFocusableElement(searchScope);
+        }
+
+        /// <summary>
+        /// Retrieves the element that should receive focus based on the specified navigation direction.
+        /// </summary>
+        /// <param name="direction"></param>
+        /// <returns></returns>
+        public IInputElement? FindNextElement(NavigationDirection direction)
+        {
+            var xyOption = new XYFocusOptions()
+            {
+                UpdateManifold = false
+            };
+
+            return FindNextFocus(direction, xyOption);
+        }
+
+        /// <summary>
+        /// Retrieves the element that should receive focus based on the specified navigation direction (cannot be used with tab navigation).
+        /// </summary>
+        /// <param name="direction">The direction that focus moves from element to element within the app UI.</param>
+        /// <param name="options">The options to help identify the next element to receive focus with the provided navigation.</param>
+        /// <returns>The next element to receive focus.</returns>
+        public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions options)
+        {
+            return FindNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
+        }
+
+        private static XYFocusOptions ValidateAndCreateFocusOptions(NavigationDirection direction, FindNextElementOptions options)
+        {
+            if (direction is not NavigationDirection.Up
+                and not NavigationDirection.Down
+                and not NavigationDirection.Left
+                and not NavigationDirection.Right)
+            {
+                throw new ArgumentOutOfRangeException(nameof(direction),
+                        $"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported");
+            }
+
+            return new XYFocusOptions
+            {
+                UpdateManifold = false,
+                SearchRoot = options.SearchRoot,
+                ExclusionRect = options.ExclusionRect,
+                FocusHintRectangle = options.FocusHintRectangle,
+                NavigationStrategyOverride = options.NavigationStrategyOverride,
+                IgnoreOcclusivity = options.IgnoreOcclusivity
+            };
+        }
+
+        internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true)
+        {
+            IInputElement? nextFocusedElement = null;
+
+            var currentlyFocusedElement = Current;
+
+            if (direction is NavigationDirection.Previous or NavigationDirection.Next || currentlyFocusedElement == null)
+            {
+                var isReverse = direction == NavigationDirection.Previous;
+                nextFocusedElement = ProcessTabStopInternal(isReverse, true);
+            }
+            else
+            {
+                if (currentlyFocusedElement is InputElement inputElement &&
+                    XYFocus.GetBoundsForRanking(inputElement, focusOptions.IgnoreClipping) is { } bounds)
+                {
+                    focusOptions.FocusedElementBounds = bounds;
+                }
+
+                nextFocusedElement = _xyFocus.GetNextFocusableElement(direction,
+                    currentlyFocusedElement as InputElement,
+                    null,
+                    updateManifolds,
+                    focusOptions);
+            }
+
+            return nextFocusedElement;
+        }
+
+        internal static IInputElement? GetFirstFocusableElementInternal(IInputElement searchStart, IInputElement? focusCandidate = null)
+        {
+            IInputElement? firstFocusableFromCallback = null;
+            var useFirstFocusableFromCallback = false;
+            if (searchStart is InputElement inputElement)
+            {
+                firstFocusableFromCallback = inputElement.GetFirstFocusableElementOverride();
+
+                if (firstFocusableFromCallback != null)
+                {
+                    useFirstFocusableFromCallback = FocusHelpers.IsFocusable(firstFocusableFromCallback) || FocusHelpers.CanHaveFocusableChildren(firstFocusableFromCallback as AvaloniaObject);
+                }
+            }
+
+            if (useFirstFocusableFromCallback)
+            {
+                if (focusCandidate == null || (GetTabIndex(firstFocusableFromCallback) < GetTabIndex(focusCandidate)))
+                {
+                    focusCandidate = firstFocusableFromCallback;
+                }
+            }
+            else
+            {
+                var children = FocusHelpers.GetInputElementChildren(searchStart as AvaloniaObject);
+
+                foreach (var child in children)
+                {
+                    if (FocusHelpers.IsVisible(child))
+                    {
+                        var hasFocusableChildren = FocusHelpers.CanHaveFocusableChildren(child as AvaloniaObject);
+                        if (FocusHelpers.IsPotentialTabStop(child))
+                        {
+                            if (focusCandidate == null && (FocusHelpers.IsFocusable(child) || hasFocusableChildren))
+                            {
+                                focusCandidate = child;
+                            }
+
+                            if (FocusHelpers.IsFocusable(child) || hasFocusableChildren)
+                            {
+                                if (focusCandidate == null || GetTabIndex(child) < GetTabIndex(focusCandidate))
+                                {
+                                    focusCandidate = child;
+                                }
+                            }
+                        }
+                        else if (hasFocusableChildren)
+                        {
+                            focusCandidate = GetFirstFocusableElementInternal(child, focusCandidate);
+                        }
+                    }
+                }
+            }
+
+            return focusCandidate;
+        }
+
+        internal static IInputElement? GetLastFocusableElementInternal(IInputElement searchStart, IInputElement? lastFocus = null)
+        {
+            IInputElement? lastFocusableFromCallback = null;
+            var useLastFocusableFromCallback = false;
+            if (searchStart is InputElement inputElement)
+            {
+                lastFocusableFromCallback = inputElement.GetLastFocusableElementOverride();
+
+                if (lastFocusableFromCallback != null)
+                {
+                    useLastFocusableFromCallback = FocusHelpers.IsFocusable(lastFocusableFromCallback) || FocusHelpers.CanHaveFocusableChildren(lastFocusableFromCallback as AvaloniaObject);
+                }
+            }
+
+            if (useLastFocusableFromCallback)
+            {
+                if (lastFocus == null || (GetTabIndex(lastFocusableFromCallback) > GetTabIndex(lastFocus)))
+                {
+                    lastFocus = lastFocusableFromCallback;
+                }
+            }
+            else
+            {
+                var children = FocusHelpers.GetInputElementChildren(searchStart as AvaloniaObject);
+
+                foreach (var child in children)
+                {
+                    if (FocusHelpers.IsVisible(child))
+                    {
+                        var hasFocusableChildren = FocusHelpers.CanHaveFocusableChildren(child as AvaloniaObject);
+                        if (FocusHelpers.IsPotentialTabStop(child))
+                        {
+                            if (lastFocus == null && (FocusHelpers.IsFocusable(child) || hasFocusableChildren))
+                            {
+                                lastFocus = child;
+                            }
+
+                            if (FocusHelpers.IsFocusable(child) || hasFocusableChildren)
+                            {
+                                if (lastFocus == null || GetTabIndex(child) >= GetTabIndex(lastFocus))
+                                {
+                                    lastFocus = child;
+                                }
+                            }
+                        }
+                        else if (hasFocusableChildren)
+                        {
+                            lastFocus = GetLastFocusableElementInternal(child, lastFocus);
+                        }
+                    }
+                }
+            }
+
+            return lastFocus;
+        }
+
+        private IInputElement? ProcessTabStopInternal(bool isReverse, bool queryOnly)
+        {
+            IInputElement? newTabStop = null;
+
+            var defaultCandidateTabStop = GetTabStopCandidateElement(isReverse, queryOnly, out var didCycleFocusAtRootVisualScope);
+
+            var isTabStopOverriden = InputElement.ProcessTabStop(_contentRoot,
+                Current,
+                defaultCandidateTabStop,
+                isReverse,
+                didCycleFocusAtRootVisualScope,
+                out var newTabStopFromCallback);
+
+            if (isTabStopOverriden)
+            {
+                newTabStop = newTabStopFromCallback;
+            }
+
+            if (!isTabStopOverriden && newTabStop == null && defaultCandidateTabStop != null)
+            {
+                newTabStop = defaultCandidateTabStop;
+            }
+
+            return newTabStop;
+        }
+
+        private IInputElement? GetTabStopCandidateElement(bool isReverse, bool queryOnly, out bool didCycleFocusAtRootVisualScope)
+        {
+            didCycleFocusAtRootVisualScope = false;
+            var currentFocus = Current;
+            IInputElement? newTabStop = null;
+            var root = this._contentRoot as IInputElement;
+
+            if (root == null)
+                return null;
+
+            bool internalCycleWorkaround = false;
+
+            if (Current != null)
+            {
+                internalCycleWorkaround = CanProcessTabStop(isReverse);
+            }
+
+            if (currentFocus == null)
+            {
+                if (!isReverse)
+                {
+                    newTabStop = GetFirstFocusableElement(root, null);
+                }
+                else
+                {
+                    newTabStop = GetLastFocusableElement(root, null);
+                }
+
+                didCycleFocusAtRootVisualScope = true;
+            }
+            else if (!isReverse)
+            {
+                newTabStop = GetNextTabStop();
+
+                if (newTabStop == null && (internalCycleWorkaround || queryOnly))
+                {
+                    newTabStop = GetFirstFocusableElement(root, null);
+
+                    didCycleFocusAtRootVisualScope = true;
+                }
+            }
+            else
+            {
+                newTabStop = GetPreviousTabStop();
+
+                if (newTabStop == null && (internalCycleWorkaround || queryOnly))
+                {
+                    newTabStop = GetLastFocusableElement(root, null);
+                    didCycleFocusAtRootVisualScope = true;
+                }
+            }
+
+            return newTabStop;
+        }
+
+        private IInputElement? GetNextTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false)
+        {
+            var focused = currentTabStop ?? Current;
+            if (focused == null || _contentRoot == null)
+            {
+                return null;
+            }
+
+            IInputElement? currentCompare = focused;
+            IInputElement? newTabStop = (focused as InputElement)?.GetNextTabStopOverride();
+
+            if (newTabStop == null && !ignoreCurrentTabStop
+                && (FocusHelpers.IsVisible(focused) && (FocusHelpers.CanHaveFocusableChildren(focused as AvaloniaObject) || FocusHelpers.CanHaveChildren(focused))))
+            {
+                newTabStop = GetFirstFocusableElement(focused, newTabStop);
+            }
+
+            if (newTabStop == null)
+            {
+                var currentPassed = false;
+                var current = focused;
+                var parent = FocusHelpers.GetFocusParent(focused);
+                var parentIsRootVisual = parent == (_contentRoot as Visual)?.VisualRoot;
+
+                while (parent != null && !parentIsRootVisual && newTabStop == null)
+                {
+                    if (IsValidTabStopSearchCandidate(current) && current is InputElement c && KeyboardNavigation.GetTabNavigation(c) == KeyboardNavigationMode.Cycle)
+                    {
+                        if (current == GetParentTabStopElement(focused))
+                        {
+                            newTabStop = GetFirstFocusableElement(focused, null);
+                        }
+                        else
+                        {
+                            newTabStop = GetFirstFocusableElement(current, current);
+                        }
+                        break;
+                    }
+
+                    if (IsValidTabStopSearchCandidate(parent) && parent is InputElement p && KeyboardNavigation.GetTabNavigation(p) == KeyboardNavigationMode.Once)
+                    {
+                        current = parent;
+                        parent = FocusHelpers.GetFocusParent(focused);
+                        if (parent == null)
+                            break;
+                    }
+                    else if (!IsValidTabStopSearchCandidate(parent))
+                    {
+                        var parentElement = GetParentTabStopElement(parent);
+                        if (parentElement == null)
+                        {
+                            parent = GetRootOfPopupSubTree(current) as IInputElement;
+
+                            if (parent != null)
+                            {
+                                newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, true, ref currentPassed, ref currentCompare);
+
+                                if (newTabStop != null && !FocusHelpers.IsFocusable(newTabStop))
+                                {
+                                    newTabStop = GetFirstFocusableElement(newTabStop, null);
+                                }
+                                if (newTabStop == null)
+                                {
+                                    newTabStop = GetFirstFocusableElement(parent, null);
+                                }
+                                break;
+                            }
+
+                            parent = (_contentRoot as Visual)?.VisualRoot as IInputElement;
+                        }
+                        else if (parentElement is InputElement pIE && KeyboardNavigation.GetTabNavigation(pIE) == KeyboardNavigationMode.None)
+                        {
+                            current = pIE;
+                            parent = FocusHelpers.GetFocusParent(current);
+                            if (parent == null)
+                                break;
+                        }
+                        else
+                        {
+                            parent = parentElement as IInputElement;
+                        }
+                    }
+
+                    newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, true, ref currentPassed, ref currentCompare);
+
+                    if (newTabStop != null && !FocusHelpers.IsFocusable(newTabStop) && FocusHelpers.CanHaveFocusableChildren(newTabStop as AvaloniaObject))
+                    {
+                        newTabStop = GetFirstFocusableElement(newTabStop, null);
+                    }
+
+                    if (newTabStop != null)
+                        break;
+
+                    if (IsValidTabStopSearchCandidate(parent))
+                    {
+                        current = parent;
+                    }
+
+                    parent = FocusHelpers.GetFocusParent(parent);
+                    currentPassed = false;
+
+                    parentIsRootVisual = parent == (_contentRoot as Visual)?.VisualRoot;
+                }
+            }
+
+            return newTabStop;
+        }
+
+        private IInputElement? GetPreviousTabStop(IInputElement? currentTabStop = null, bool ignoreCurrentTabStop = false)
+        {
+            var focused = currentTabStop ?? Current;
+            if (focused == null || _contentRoot == null)
+            {
+                return null;
+            }
+            IInputElement? newTabStop = (focused as InputElement)?.GetPreviousTabStopOverride();
+            IInputElement? currentCompare = focused;
+
+            if (newTabStop == null)
+            {
+                var currentPassed = false;
+                var current = focused;
+                var parent = FocusHelpers.GetFocusParent(focused);
+                var parentIsRootVisual = parent == (_contentRoot as Visual)?.VisualRoot;
+
+                while (parent != null && !parentIsRootVisual && newTabStop == null)
+                {
+                    if (IsValidTabStopSearchCandidate(current) && current is InputElement c && KeyboardNavigation.GetTabNavigation(c) == KeyboardNavigationMode.Cycle)
+                    {
+                        newTabStop = GetFirstFocusableElement(current, current);
+                        break;
+                    }
+
+                    if (IsValidTabStopSearchCandidate(parent) && parent is InputElement p && KeyboardNavigation.GetTabNavigation(p) == KeyboardNavigationMode.Once)
+                    {
+                        if (FocusHelpers.IsFocusable(parent))
+                        {
+                            newTabStop = parent;
+                        }
+                        else
+                        {
+                            current = parent;
+                            parent = FocusHelpers.GetFocusParent(focused);
+                            if (parent == null)
+                                break;
+                        }
+                    }
+                    else if (!IsValidTabStopSearchCandidate(parent))
+                    {
+                        var parentElement = GetParentTabStopElement(parent);
+                        if (parentElement == null)
+                        {
+                            parent = GetRootOfPopupSubTree(current) as IInputElement;
+
+                            if (parent != null)
+                            {
+                                newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, false, ref currentPassed, ref currentCompare);
+
+                                if (newTabStop != null && !FocusHelpers.IsFocusable(newTabStop))
+                                {
+                                    newTabStop = GetLastFocusableElement(newTabStop, null);
+                                }
+                                if (newTabStop == null)
+                                {
+                                    newTabStop = GetLastFocusableElement(parent, null);
+                                }
+                                break;
+                            }
+
+                            parent = (_contentRoot as Visual)?.VisualRoot as IInputElement;
+                        }
+                        else if (parentElement is InputElement pIE && KeyboardNavigation.GetTabNavigation(pIE) == KeyboardNavigationMode.None)
+                        {
+                            if (FocusHelpers.IsFocusable(parent))
+                            {
+                                newTabStop = parent;
+                            }
+                            else
+                            {
+                                current = parent;
+                                parent = FocusHelpers.GetFocusParent(focused);
+                                if (parent == null)
+                                    break;
+                            }
+                        }
+                        else
+                        {
+                            parent = parentElement as IInputElement;
+                        }
+                    }
+
+                    newTabStop = GetNextOrPreviousTabStopInternal(parent, current, newTabStop, false, ref currentPassed, ref currentCompare);
+
+                    if (newTabStop == null && FocusHelpers.IsPotentialTabStop(parent) && FocusHelpers.IsFocusable(parent))
+                    {
+                        if (parent is InputElement iE && KeyboardNavigation.GetTabNavigation(iE) == KeyboardNavigationMode.Cycle)
+                        {
+                            newTabStop = GetLastFocusableElement(parent, null);
+                        }
+                        else
+                        {
+                            newTabStop = parent;
+                        }
+                    }
+                    else
+                    {
+                        if (newTabStop != null && FocusHelpers.CanHaveFocusableChildren(newTabStop as AvaloniaObject))
+                        {
+                            newTabStop = GetLastFocusableElement(newTabStop, null);
+                        }
+                    }
+
+                    if (newTabStop != null)
+                        break;
+
+                    if (IsValidTabStopSearchCandidate(parent))
+                    {
+                        current = parent;
+                    }
+
+                    parent = FocusHelpers.GetFocusParent(parent);
+                    currentPassed = false;
+                }
+            }
+
+            return newTabStop;
+        }
+
+        private IInputElement? GetNextOrPreviousTabStopInternal(IInputElement? parent, IInputElement? current, IInputElement? candidate, bool findNext, ref bool currentPassed, ref IInputElement? currentCompare)
+        {
+            var newTabStop = candidate;
+            IInputElement? childStop = null;
+            int compareIndexResult = 0;
+            bool compareCurrentForPreviousElement = false;
+
+            if (IsValidTabStopSearchCandidate(current))
+            {
+                currentCompare = current;
+            }
+
+            if (parent != null)
+            {
+                bool foundCurrent = false;
+                foreach (var child in FocusHelpers.GetInputElementChildren(parent as AvaloniaObject))
+                {
+                    childStop = null;
+                    compareCurrentForPreviousElement = false;
+                    if (child == current)
+                    {
+                        foundCurrent = true;
+                        currentPassed = true;
+                        continue;
+                    }
+
+                    if (FocusHelpers.IsVisible(child))
+                    {
+                        if (child == current)
+                        {
+                            foundCurrent = true;
+                            currentPassed = true;
+                            continue;
+                        }
+
+                        if (IsValidTabStopSearchCandidate(child))
+                        {
+                            if (!FocusHelpers.IsPotentialTabStop(child))
+                            {
+                                childStop = GetNextOrPreviousTabStopInternal(childStop, current, newTabStop, findNext, ref currentPassed, ref currentCompare);
+                                compareCurrentForPreviousElement = true;
+                            }
+                            else
+                            {
+                                childStop = child;
+                            }
+                        }
+                        else if (FocusHelpers.CanHaveFocusableChildren(child as AvaloniaObject))
+                        {
+                            childStop = GetNextOrPreviousTabStopInternal(child, current, newTabStop, findNext, ref currentPassed, ref currentCompare);
+                            compareCurrentForPreviousElement = true;
+                        }
+                    }
+
+                    if (childStop != null && (FocusHelpers.IsFocusable(childStop) || FocusHelpers.CanHaveFocusableChildren(childStop as AvaloniaObject)))
+                    {
+                        compareIndexResult = CompareTabIndex(childStop, currentCompare);
+
+                        if (findNext)
+                        {
+                            if (compareIndexResult > 0 || ((foundCurrent || currentPassed) && compareIndexResult == 0))
+                            {
+                                if (newTabStop != null)
+                                {
+                                    if (CompareTabIndex(childStop, newTabStop) < 0)
+                                    {
+                                        newTabStop = childStop;
+                                    }
+                                }
+                                else
+                                {
+                                    newTabStop = childStop;
+                                }
+                            }
+                        }
+                        else
+                        {
+                            if (compareIndexResult < 0 || (((foundCurrent || currentPassed) || compareCurrentForPreviousElement) && compareIndexResult == 0))
+                            {
+                                if (newTabStop != null)
+                                {
+                                    if (CompareTabIndex(childStop, newTabStop) >= 0)
+                                    {
+                                        newTabStop = childStop;
+                                    }
+                                }
+                                else
+                                {
+                                    newTabStop = childStop;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return newTabStop;
+        }
+
+        private static int CompareTabIndex(IInputElement? control1, IInputElement? control2)
+        {
+            return GetTabIndex(control1).CompareTo(GetTabIndex(control2));
+        }
+
+        private static int GetTabIndex(IInputElement? element)
+        {
+            if (element is InputElement inputElement)
+                return inputElement.TabIndex;
+
+            return int.MaxValue;
+        }
+
+        private bool CanProcessTabStop(bool isReverse)
+        {
+            bool isFocusOnFirst = false;
+            bool isFocusOnLast = false;
+            bool canProcessTab = true;
+            if (IsFocusedElementInPopup())
+            {
+                return true;
+            }
+
+            if (isReverse)
+            {
+                isFocusOnFirst = IsFocusOnFirstTabStop();
+            }
+            else
+            {
+                isFocusOnLast = IsFocusOnLastTabStop();
+            }
+
+            if (isFocusOnFirst || isFocusOnLast)
+            {
+                canProcessTab = false;
+            }
+
+            if (canProcessTab)
+            {
+                var edge = GetFirstFocusableElementFromRoot(!isReverse);
+
+                if (edge != null)
+                {
+                    var edgeParent = GetParentTabStopElement(edge);
+                    if (edgeParent is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Once && edgeParent == GetParentTabStopElement(Current))
+                    {
+                        canProcessTab = false;
+                    }
+                }
+                else
+                {
+                    canProcessTab = false;
+                }
+            }
+            else
+            {
+                if (isFocusOnLast || isFocusOnFirst)
+                {
+                    if (Current is InputElement inputElement && KeyboardNavigation.GetTabNavigation(inputElement) == KeyboardNavigationMode.Cycle)
+                    {
+                        canProcessTab = true;
+                    }
+                    else
+                    {
+                        var focusedParent = GetParentTabStopElement(Current);
+                        while (focusedParent != null)
+                        {
+                            if (focusedParent is InputElement iE && KeyboardNavigation.GetTabNavigation(iE) == KeyboardNavigationMode.Cycle)
+                            {
+                                canProcessTab = true;
+                                break;
+                            }
+
+                            focusedParent = GetParentTabStopElement(focusedParent as IInputElement);
+                        }
+                    }
+                }
+            }
+
+            return canProcessTab;
+        }
+
+        private AvaloniaObject? GetParentTabStopElement(IInputElement? current)
+        {
+            if (current != null)
+            {
+                var parent = FocusHelpers.GetFocusParent(current);
+
+                while (parent != null)
+                {
+                    if (IsValidTabStopSearchCandidate(parent) && parent is InputElement element)
+                    {
+                        return element;
+                    }
+
+                    parent = FocusHelpers.GetFocusParent(parent);
+                }
+            }
+
+            return null;
+        }
+
+        private bool IsValidTabStopSearchCandidate(IInputElement? element)
+        {
+            var isValid = FocusHelpers.IsPotentialTabStop(element);
+
+            if (!isValid)
+            {
+                isValid = (element as InputElement)?.IsSet(KeyboardNavigation.TabNavigationProperty) ?? false;
+            }
+
+            return isValid;
+        }
+
+        private IInputElement? GetFirstFocusableElementFromRoot(bool isReverse)
+        {
+            var root = (_contentRoot as Visual)?.VisualRoot as IInputElement;
+
+            if (root != null)
+                return !isReverse ? GetFirstFocusableElement(root, null) : GetLastFocusableElement(root, null);
+
+            return null;
+        }
+
+        private bool IsFocusOnLastTabStop()
+        {
+            if (Current == null || _contentRoot is not Visual visual)
+                return false;
+            var root = visual.VisualRoot as IInputElement;
+
+            Debug.Assert(root != null);
+
+            var lastFocus = GetLastFocusableElement(root, null);
+
+            return lastFocus == Current;
+        }
+
+        private bool IsFocusOnFirstTabStop()
+        {
+            if (Current == null || _contentRoot is not Visual visual)
+                return false;
+            var root = visual.VisualRoot as IInputElement;
+
+            Debug.Assert(root != null);
+
+            var firstFocus = GetFirstFocusableElement(root, null);
+
+            return firstFocus == Current;
+        }
+
+        private static IInputElement? GetFirstFocusableElement(IInputElement searchStart, IInputElement? firstFocus = null)
+        {
+            firstFocus = GetFirstFocusableElementInternal(searchStart, firstFocus);
+
+            if (firstFocus != null && !firstFocus.Focusable && FocusHelpers.CanHaveFocusableChildren(firstFocus as AvaloniaObject))
+            {
+                firstFocus = GetFirstFocusableElement(firstFocus, null);
+            }
+
+            return firstFocus;
+        }
+
+        private IInputElement? GetLastFocusableElement(IInputElement searchStart, IInputElement? lastFocus = null)
+        {
+            lastFocus = GetLastFocusableElementInternal(searchStart, lastFocus);
+
+            if (lastFocus != null && !lastFocus.Focusable && FocusHelpers.CanHaveFocusableChildren(lastFocus as AvaloniaObject))
+            {
+                lastFocus = GetLastFocusableElement(lastFocus, null);
+            }
+
+            return lastFocus;
+        }
+
+        private bool IsFocusedElementInPopup() => Current != null && GetRootOfPopupSubTree(Current) != null;
+
+        private Visual? GetRootOfPopupSubTree(IInputElement? current)
+        {
+            //TODO Popup api
+            return null;
+        }
+
+        private bool FindAndSetNextFocus(NavigationDirection direction, XYFocusOptions xYFocusOptions)
+        {
+            var focusChanged = false;
+            if (xYFocusOptions.UpdateManifoldsFromFocusHintRect && xYFocusOptions.FocusHintRectangle != null)
+            {
+                _xyFocus.SetManifoldsFromBounds(xYFocusOptions.FocusHintRectangle ?? default);
+            }
+
+            if (FindNextFocus(direction, xYFocusOptions, false) is { } nextFocusedElement)
+            {
+                focusChanged = nextFocusedElement.Focus();
+
+                if (focusChanged && xYFocusOptions.UpdateManifold && nextFocusedElement is InputElement inputElement)
+                {
+                    var bounds = xYFocusOptions.FocusHintRectangle ?? xYFocusOptions.FocusedElementBounds ?? default;
+
+                    _xyFocus.UpdateManifolds(direction, bounds, inputElement, xYFocusOptions.IgnoreClipping);
+                }
+            }
+
+            return focusChanged;
+
+        }
     }
 }

+ 159 - 13
src/Avalonia.Base/Input/InputElement.cs

@@ -51,7 +51,7 @@ namespace Avalonia.Input
             AvaloniaProperty.RegisterDirect<InputElement, bool>(
                 nameof(IsKeyboardFocusWithin),
                 o => o.IsKeyboardFocusWithin);
-        
+
         /// <summary>
         /// Defines the <see cref="IsFocused"/> property.
         /// </summary>
@@ -129,7 +129,7 @@ namespace Avalonia.Input
             RoutedEvent.Register<InputElement, TextInputEventArgs>(
                 nameof(TextInput),
                 RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
-        
+
         /// <summary>
         /// Defines the <see cref="TextInputMethodClientRequested"/> event.
         /// </summary>
@@ -177,13 +177,13 @@ namespace Avalonia.Input
             RoutedEvent.Register<InputElement, PointerReleasedEventArgs>(
                 nameof(PointerReleased),
                 RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
-        
+
         /// <summary>
         /// Defines the <see cref="PointerCaptureLost"/> routed event.
         /// </summary>
         public static readonly RoutedEvent<PointerCaptureLostEventArgs> PointerCaptureLostEvent =
             RoutedEvent.Register<InputElement, PointerCaptureLostEventArgs>(
-                nameof(PointerCaptureLost), 
+                nameof(PointerCaptureLost),
                 RoutingStrategies.Direct);
 
         /// <summary>
@@ -253,8 +253,8 @@ namespace Avalonia.Input
             PointerPressedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true);
             PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerReleased(e), handledEventsToo: true);
             PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerCaptureLost(e), handledEventsToo: true);
-            
-            
+
+
             // Access Key Handling
             AccessKeyHandler.AccessKeyEvent.AddClassHandler<InputElement>((x, e) => x.OnAccessKey(e));
         }
@@ -469,7 +469,7 @@ namespace Avalonia.Input
         public bool IsKeyboardFocusWithin
         {
             get => _isKeyboardFocusWithin;
-            internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value); 
+            internal set => SetAndRaise(IsKeyboardFocusWithinProperty, ref _isKeyboardFocusWithin, value);
         }
 
         /// <summary>
@@ -517,7 +517,7 @@ namespace Avalonia.Input
                 SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
                 PseudoClasses.Set(":disabled", !value);
 
-                if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is {} focusManager
+                if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager
                     && Equals(focusManager.GetFocusedElement(), this))
                 {
                     focusManager.ClearFocus();
@@ -554,7 +554,7 @@ namespace Avalonia.Input
         /// <inheritdoc />
         public bool Focus(NavigationMethod method = NavigationMethod.Unspecified, KeyModifiers keyModifiers = KeyModifiers.None)
         {
-            return FocusManager.GetFocusManager(this)?.Focus(this, method, keyModifiers) ?? false; 
+            return FocusManager.GetFocusManager(this)?.Focus(this, method, keyModifiers) ?? false;
         }
 
         /// <inheritdoc/>
@@ -564,7 +564,7 @@ namespace Avalonia.Input
 
             if (IsFocused)
             {
-                FocusManager.GetFocusManager(this)?.ClearFocusOnElementRemoved(this, e.Parent);
+                FocusManager.GetFocusManager(e.Root as IInputElement)?.ClearFocusOnElementRemoved(this, e.Parent);
             }
 
             IsKeyboardFocusWithin = false;
@@ -630,7 +630,7 @@ namespace Avalonia.Input
         /// </summary>
         /// <param name="e">Data about the event.</param>
         protected virtual void OnLostFocus(RoutedEventArgs e)
-        {            
+        {
         }
 
         /// <summary>
@@ -746,6 +746,30 @@ namespace Avalonia.Input
                 }
         }
 
+        /// <summary>
+        /// Called when FocusManager get the next TabStop to interact with the focused control.
+        /// </summary>
+        /// <returns>Next tab stop.</returns>
+        protected internal virtual InputElement? GetNextTabStopOverride() => null;
+
+        /// <summary>
+        /// Called when FocusManager get the previous TabStop to interact with the focused control.
+        /// </summary>
+        /// <returns>Previous tab stop.</returns>
+        protected internal virtual InputElement? GetPreviousTabStopOverride() => null;
+
+        /// <summary>
+        /// Called when FocusManager is looking for the first focusable element from the specified search scope.
+        /// </summary>
+        /// <returns>First focusable element if available.</returns>
+        protected internal virtual InputElement? GetFirstFocusableElementOverride() => null;
+
+        /// <summary>
+        /// Called when FocusManager is looking for the last focusable element from the specified search scope.
+        /// </summary>
+        /// <returns>Last focusable element if available/>.</returns>
+        protected internal virtual InputElement? GetLastFocusableElementOverride() => null;
+
         /// <summary>
         /// Invoked when an unhandled <see cref="PointerCaptureLostEvent"/> reaches an element in its 
         /// route that is derived from this class. Implement this method to add class handling 
@@ -757,6 +781,127 @@ namespace Avalonia.Input
 
         }
 
+        internal static bool ProcessTabStop(IInputElement? contentRoot,
+            IInputElement? focusedElement,
+            IInputElement? candidateTabStopElement,
+            bool isReverse,
+            bool didCycleFocusAtRootVisual,
+            out IInputElement? newTabStop)
+        {
+            newTabStop = null;
+            bool isTabStopOverridden = false;
+            bool isCandidateTabStopOverridden = false;
+            IInputElement? currentFocusedTarget = focusedElement;
+            InputElement? focusedTargetAsIE = focusedElement as InputElement;
+            InputElement? candidateTargetAsIE = candidateTabStopElement as InputElement;
+            InputElement? newCandidateTargetAsIE = null;
+            IInputElement? newCandidateTabStop = null;
+            IInputElement? spNewTabStop = null;
+
+            if (focusedTargetAsIE != null)
+            {
+                isTabStopOverridden = focusedTargetAsIE.ProcessTabStopInternal(candidateTabStopElement, isReverse, didCycleFocusAtRootVisual, out spNewTabStop);
+            }
+
+            if (!isTabStopOverridden && candidateTargetAsIE != null)
+            {
+                isTabStopOverridden = candidateTargetAsIE.ProcessCandidateTabStopInternal(focusedElement, null, isReverse, out spNewTabStop);
+            }
+            else if (isTabStopOverridden && newTabStop != null)
+            {
+                newCandidateTargetAsIE = spNewTabStop as InputElement;
+                if (newCandidateTargetAsIE != null)
+                {
+                    isCandidateTabStopOverridden = newCandidateTargetAsIE.ProcessCandidateTabStopInternal(focusedElement, spNewTabStop, isReverse, out newCandidateTabStop);
+                }
+            }
+
+            if (isCandidateTabStopOverridden)
+            {
+                if (newCandidateTabStop != null)
+                {
+                    newTabStop = newCandidateTabStop;
+                }
+
+                isTabStopOverridden = true;
+            }
+            else if (isTabStopOverridden)
+            {
+                if (newTabStop != null)
+                {
+                    newTabStop = spNewTabStop;
+                }
+
+                isTabStopOverridden = true;
+            }
+
+            return isTabStopOverridden;
+        }
+
+        private bool ProcessTabStopInternal(IInputElement? candidateTabStopElement,
+            bool isReverse,
+            bool didCycleFocusAtRootVisual,
+            out IInputElement? newTabStop)
+        {
+            InputElement? current = this;
+
+            newTabStop = null;
+            var candidateTabStopOverridden = false;
+
+            while (current != null && !candidateTabStopOverridden)
+            {
+                candidateTabStopOverridden = current.ProcessTabStopOverride(this,
+                    candidateTabStopElement,
+                    isReverse,
+                    didCycleFocusAtRootVisual,
+                    ref newTabStop);
+
+                current = (current as Visual)?.Parent as InputElement;
+            }
+            return candidateTabStopOverridden;
+        }
+
+        private bool ProcessCandidateTabStopInternal(IInputElement? currentTabStop,
+            IInputElement? overridenCandidateTabStopElement,
+            bool isReverse,
+            out IInputElement? newTabStop)
+        {
+            InputElement? current = this;
+
+            newTabStop = null;
+            var candidateTabStopOverridden = false;
+
+            while (current != null && !candidateTabStopOverridden)
+            {
+                candidateTabStopOverridden = current.ProcessCandidateTabStopOverride(currentTabStop,
+                    this,
+                    overridenCandidateTabStopElement,
+                    isReverse,
+                    ref newTabStop);
+
+                current = (current as Visual)?.Parent as InputElement;
+            }
+            return candidateTabStopOverridden;
+        }
+
+        protected internal virtual bool ProcessTabStopOverride(IInputElement? focusedElement,
+            IInputElement? candidateTabStopElement,
+            bool isReverse,
+            bool didCycleFocusAtRootVisual,
+            ref IInputElement? newTabStop)
+        {
+            return false;
+        }
+
+        protected internal virtual bool ProcessCandidateTabStopOverride(IInputElement? focusedElement,
+            IInputElement? candidateTabStopElement,
+            IInputElement? overridenCandidateTabStopElement,
+            bool isReverse,
+            ref IInputElement? newTabStop)
+        {
+            return false;
+        }
+
         /// <summary>
         /// Invoked when an unhandled <see cref="PointerWheelChangedEvent"/> reaches an element in its 
         /// route that is derived from this class. Implement this method to add class handling 
@@ -765,6 +910,7 @@ namespace Avalonia.Input
         /// <param name="e">Data about the event.</param>
         protected virtual void OnPointerWheelChanged(PointerWheelEventArgs e)
         {
+
         }
 
         /// <summary>
@@ -884,7 +1030,7 @@ namespace Avalonia.Input
 
             // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
             // will cause extra allocations and overhead.
-            
+
             var children = VisualChildren;
 
             // ReSharper disable once ForCanBeConvertedToForeach
@@ -903,7 +1049,7 @@ namespace Avalonia.Input
                 PseudoClasses.Set(":focus", isFocused.Value);
                 PseudoClasses.Set(":focus-visible", _isFocusVisible);
             }
-            
+
             if (isPointerOver.HasValue)
             {
                 PseudoClasses.Set(":pointerover", isPointerOver.Value);

+ 9 - 8
src/Avalonia.Base/Input/Navigation/TabNavigation.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Input.Navigation
@@ -225,8 +226,8 @@ namespace Avalonia.Input.Navigation
             {
                 if (e is Visual elementAsVisual)
                 {
-                    var children = elementAsVisual.VisualChildren;
-                    var count = children.Count;
+                    var children = FocusHelpers.GetInputElementChildren(elementAsVisual).ToArray();
+                    var count = children.Length;
 
                     for (int i = 0; i < count; i++)
                     {
@@ -261,8 +262,8 @@ namespace Avalonia.Input.Navigation
             {
                 if (e is Visual elementAsVisual)
                 {
-                    var children = elementAsVisual.VisualChildren;
-                    var count = children.Count;
+                    var children = FocusHelpers.GetInputElementChildren(elementAsVisual).ToArray();
+                    var count = children.Length;
 
                     for (int i = count - 1; i >= 0; i--)
                     {
@@ -284,7 +285,7 @@ namespace Avalonia.Input.Navigation
             return null;
         }
 
-        private static IInputElement? GetFirstTabInGroup(IInputElement container)
+        internal static IInputElement? GetFirstTabInGroup(IInputElement container)
         {
             IInputElement? firstTabElement = null;
             int minIndexFirstTab = int.MinValue;
@@ -372,7 +373,7 @@ namespace Avalonia.Input.Navigation
         {
             if (GetParent(e) is Visual parentAsVisual && e is Visual elementAsVisual)
             {
-                var children = parentAsVisual.VisualChildren;
+                var children = FocusHelpers.GetInputElementChildren(parentAsVisual).ToList();
                 var count = children.Count;
                 var i = 0;
 
@@ -576,7 +577,7 @@ namespace Avalonia.Input.Navigation
         {
             if (GetParent(e) is Visual parentAsVisual && e is Visual elementAsVisual)
             {
-                var children = parentAsVisual.VisualChildren;
+                var children = FocusHelpers.GetInputElementChildren(parentAsVisual).ToList();
                 var count = children.Count;
                 IInputElement? prev = null;
                 
@@ -646,7 +647,7 @@ namespace Avalonia.Input.Navigation
         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)
+        internal static bool IsTabStop(IInputElement e)
         {
             if (e is InputElement ie)
                 return ie.Focusable && KeyboardNavigation.GetIsTabStop(ie) && ie.IsVisible && ie.IsEffectivelyEnabled;

+ 1 - 1
src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs

@@ -118,7 +118,7 @@ public partial class XYFocus
         return !visibleBounds.Intersects(elementBounds);
     }
 
-    private static Rect? GetBoundsForRanking(InputElement element, bool ignoreClipping)
+    internal static Rect? GetBoundsForRanking(InputElement element, bool ignoreClipping)
     {
         if (element.GetTransformedBounds() is { } bounds)
         {

+ 8 - 8
src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs

@@ -23,21 +23,21 @@ public partial class XYFocus
         
     }
     
-    private XYFocusAlgorithms.XYFocusManifolds mManifolds = new();
+    private XYFocusAlgorithms.XYFocusManifolds _manifolds = new();
     private PooledList<XYFocusParams> _pooledCandidates = new();
 
     private static readonly XYFocus _instance = new();
     
     internal XYFocusAlgorithms.XYFocusManifolds ResetManifolds()
     {
-        mManifolds.Reset();
-        return mManifolds;
+        _manifolds.Reset();
+        return _manifolds;
     }
 
     internal void SetManifoldsFromBounds(Rect bounds)
     {
-        mManifolds.VManifold = (bounds.Left, bounds.Right);
-        mManifolds.HManifold = (bounds.Top, bounds.Bottom);
+        _manifolds.VManifold = (bounds.Left, bounds.Right);
+        _manifolds.HManifold = (bounds.Top, bounds.Bottom);
     }
 
     internal void UpdateManifolds(
@@ -47,7 +47,7 @@ public partial class XYFocus
         bool ignoreClipping)
     {
         var candidateBounds = GetBoundsForRanking(candidate, ignoreClipping)!.Value;
-        XYFocusAlgorithms.UpdateManifolds(direction, elementBounds, candidateBounds, mManifolds);
+        XYFocusAlgorithms.UpdateManifolds(direction, elementBounds, candidateBounds, _manifolds);
     }
 
     internal static InputElement? TryDirectionalFocus(
@@ -261,7 +261,7 @@ public partial class XYFocus
                 if (updateManifolds)
                 {
                     // Update the manifolds with the newly selected focus
-                    XYFocusAlgorithms.UpdateManifolds(direction, bounds, param.Bounds, mManifolds);
+                    XYFocusAlgorithms.UpdateManifolds(direction, bounds, param.Bounds, _manifolds);
                 }
 
                 break;
@@ -346,7 +346,7 @@ public partial class XYFocus
                     XYFocusAlgorithms.ShouldCandidateBeConsideredForRanking(bounds, candidateBounds, maxRootBoundsDistance,
                         direction, exclusionBounds, ignoreCone))
                 {
-                    candidate.Score = XYFocusAlgorithms.GetScoreProjection(direction, bounds, candidateBounds, mManifolds, maxRootBoundsDistance);
+                    candidate.Score = XYFocusAlgorithms.GetScoreProjection(direction, bounds, candidateBounds, _manifolds, maxRootBoundsDistance);
                 }
                 else if (mode == XYFocusNavigationStrategy.NavigationDirectionDistance ||
                          mode == XYFocusNavigationStrategy.RectilinearDistance)

+ 0 - 2
src/Avalonia.Controls/Application.cs

@@ -265,7 +265,6 @@ namespace Avalonia
         public virtual void RegisterServices()
         {
             AvaloniaSynchronizationContext.InstallIfNeeded();
-            var focusManager = new FocusManager();
             InputManager = new InputManager();
 
             if (PlatformSettings is { } settings)
@@ -279,7 +278,6 @@ namespace Avalonia
                 .Bind<IGlobalDataTemplates>().ToConstant(this)
                 .Bind<IGlobalStyles>().ToConstant(this)
                 .Bind<IThemeVariantHost>().ToConstant(this)
-                .Bind<IFocusManager>().ToConstant(focusManager)
                 .Bind<IInputManager>().ToConstant(InputManager)
                 .Bind< IToolTipService>().ToConstant(new ToolTipService(InputManager))
                 .Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()

+ 2 - 1
src/Avalonia.Controls/TopLevel.cs

@@ -590,7 +590,7 @@ namespace Avalonia.Controls
         public IClipboard? Clipboard => PlatformImpl?.TryGetFeature<IClipboard>();
 
         /// <inheritdoc />
-        public IFocusManager? FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
+        public IFocusManager? FocusManager => _focusManager ??= new FocusManager(this);
 
         /// <inheritdoc />
         public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService<IPlatformSettings>();
@@ -665,6 +665,7 @@ namespace Avalonia.Controls
         }
 
         private IDisposable? _insetsPaddings;
+        private FocusManager? _focusManager;
 
         private void InvalidateChildInsetsPadding()
         {

+ 251 - 0
tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs

@@ -702,6 +702,257 @@ namespace Avalonia.Base.UnitTests.Input
             Assert.Same(innerButton, focusManager.GetFocusedElement());
         }
 
+        [Fact]
+        public void Can_Get_First_Focusable_Element()
+        {
+            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 firstFocusable = FocusManager.FindFirstFocusableElement(container);
+
+                Assert.Equal(target1, firstFocusable);
+
+                firstFocusable = (root.FocusManager as FocusManager)?.FindFirstFocusableElement();
+
+                Assert.Equal(target1, firstFocusable);
+            }
+        }
+
+        [Fact]
+        public void Can_Get_Last_Focusable_Element()
+        {
+            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 lastFocusable = FocusManager.FindLastFocusableElement(container);
+
+                Assert.Equal(target4, lastFocusable);
+
+                lastFocusable = (root.FocusManager as FocusManager)?.FindLastFocusableElement();
+
+                Assert.Equal(target4, lastFocusable);
+            }
+        }
+
+        [Fact]
+        public void Can_Get_Next_Element()
+        {
+            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);
+                target1.Focus();
+
+                var next = focusManager.FindNextElement(NavigationDirection.Next);
+
+                Assert.Equal(next, target2);
+            }
+        }
+
+        [Fact]
+        public void Can_Get_Next_Element_With_Options()
+        {
+            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 seachStack = new StackPanel()
+                {
+                    Children =
+                    {
+                        target3,
+                        target4
+                    }
+                };
+                var container = new StackPanel
+                {
+                    Orientation = Avalonia.Layout.Orientation.Horizontal,
+                    Children =
+                    {
+                        target1,
+                        target2,
+                        seachStack,
+                        target5
+                    }
+                };
+                var root = new TestRoot
+                {
+                    Child = container
+                };
+
+                root.InvalidateMeasure();
+                root.ExecuteInitialLayoutPass();
+
+                var focusManager = FocusManager.GetFocusManager(container);
+                target1.Focus();
+
+                var options = new FindNextElementOptions()
+                {
+                    SearchRoot = seachStack
+                };
+
+                // Search root is right of the current focus, should return the first focusable element in the search root
+                var next = focusManager.FindNextElement(NavigationDirection.Right, options);
+
+                Assert.Equal(next, target3);
+
+                target5.Focus();
+
+                // Search root is right of the current focus, should return the first focusable element in the search root
+                next = focusManager.FindNextElement(NavigationDirection.Left, options);
+
+                Assert.Equal(next, target3);
+
+                // Search root isn't to the right of the current focus, should return null
+                next = focusManager.FindNextElement(NavigationDirection.Right, options);
+
+                Assert.Null(next);
+            }
+        }
+
+        [Fact]
+        public void Focus_Should_Move_According_To_Direction()
+        {
+            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);
+
+                var hasMoved = focusManager.TryMoveFocus(NavigationDirection.Next);
+
+                Assert.True(target1.IsFocused);
+                Assert.True(hasMoved);
+
+                hasMoved = focusManager.TryMoveFocus(NavigationDirection.Previous);
+
+                Assert.True(target4.IsFocused);
+                Assert.True(hasMoved);
+            }
+        }
+
+        [Fact]
+        public void Focus_Should_Move_According_To_XY_Direction()
+        {
+            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 center = new Button
+                {
+                    [XYFocus.LeftProperty] = target1,
+                    [XYFocus.RightProperty] = target2,
+                    [XYFocus.UpProperty] = target3,
+                    [XYFocus.DownProperty] = target4,
+                };
+                var container =  new Canvas
+                {
+                    Children =
+                    {
+                        target1,
+                        target2,
+                        target3,
+                        target4,
+                        center
+                    }
+                };
+
+                var root = new TestRoot
+                {
+                    Child = container
+                };
+
+                var focusManager = FocusManager.GetFocusManager(container);
+
+                center.Focus();
+
+                var options = new FindNextElementOptions()
+                {
+                    SearchRoot = container
+                };
+
+                var hasMoved = focusManager.TryMoveFocus(NavigationDirection.Up, options);
+                Assert.True(target3.IsFocused);
+                Assert.True(hasMoved);
+            }
+        }
+
         private class TestFocusScope : Panel, IFocusScope
         {
         }

+ 0 - 1
tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs

@@ -24,7 +24,6 @@ namespace Avalonia.Base.UnitTests.Input
         {
             using var app = UnitTestApplication.Start(new TestServices(
                 inputManager: new InputManager(),
-                focusManager: new FocusManager(),
                 renderInterface: new HeadlessPlatformRenderInterface()));
 
             var renderer = new Mock<IHitTester>();

+ 0 - 1
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@@ -1261,7 +1261,6 @@ namespace Avalonia.Controls.UnitTests
         }
 
         private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
-            focusManager: new FocusManager(),
             keyboardDevice: () => new KeyboardDevice(),
             keyboardNavigation: () => new KeyboardNavigationHandler(),
             inputManager: new InputManager(),

+ 1 - 2
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -334,13 +334,12 @@ namespace Avalonia.Controls.UnitTests
                     })
                 },
             };
-            kd.SetFocusedElement(target, NavigationMethod.Unspecified, KeyModifiers.None);
-            
 
             root.ApplyTemplate();
             root.Presenter.UpdateChild();
             target.ApplyTemplate();
             target.Presenter.UpdateChild();
+            kd.SetFocusedElement(target, NavigationMethod.Unspecified, KeyModifiers.None);
 
             Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
 

+ 0 - 1
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -672,7 +672,6 @@ namespace Avalonia.Controls.UnitTests
             windowImpl.Setup(x => x.TryGetFeature(It.Is<Type>(t => t == typeof(IScreenImpl)))).Returns(screenImpl.Object);
 
             var services = TestServices.StyledWindow.With(
-                                        focusManager: new FocusManager(),
                                         keyboardDevice: () => new KeyboardDevice(),
                                         inputManager: new InputManager(),
                                         windowImpl: windowImpl.Object,

+ 0 - 1
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@@ -647,7 +647,6 @@ namespace Avalonia.Controls.UnitTests
             return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
                 new MockWindowingPlatform(null,
                     x => UseOverlayPopups ? null : MockWindowingPlatform.CreatePopupMock(x).Object),
-                    focusManager: new FocusManager(),
                     keyboardDevice: () => new KeyboardDevice()));
         }
 

+ 0 - 1
tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs

@@ -90,7 +90,6 @@ namespace Avalonia.Controls.UnitTests
                     windowingPlatform: new MockWindowingPlatform(
                         null,
                         window => MockWindowingPlatform.CreatePopupMock(window).Object),
-                focusManager: new FocusManager(),
                 keyboardDevice: () => new KeyboardDevice()));
         }
         

+ 0 - 1
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -1233,7 +1233,6 @@ namespace Avalonia.Controls.UnitTests
         {
             return UnitTestApplication.Start(
                 TestServices.MockThreadingInterface.With(
-                    focusManager: new FocusManager(),
                     fontManagerImpl: new HeadlessFontManagerStub(),
                     keyboardDevice: () => new KeyboardDevice(),
                     keyboardNavigation: () => new KeyboardNavigationHandler(),

+ 0 - 2
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@@ -1102,7 +1102,6 @@ namespace Avalonia.Controls.UnitTests
         public void Tab_Navigation_Should_Move_To_First_Item_When_No_Anchor_Element_Selected()
         {
             var services = TestServices.StyledWindow.With(
-                focusManager: new FocusManager(),
                 keyboardDevice: () => new KeyboardDevice());
             using var app = UnitTestApplication.Start(services);
 
@@ -1146,7 +1145,6 @@ namespace Avalonia.Controls.UnitTests
         public void Tab_Navigation_Should_Move_To_Anchor_Element()
         {
             var services = TestServices.StyledWindow.With(
-                focusManager: new FocusManager(),
                 keyboardDevice: () => new KeyboardDevice());
             using var app = UnitTestApplication.Start(services);
 

+ 0 - 1
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@@ -921,7 +921,6 @@ namespace Avalonia.Controls.UnitTests
         }
 
         private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
-            focusManager: new FocusManager(),
             keyboardDevice: () => new KeyboardDevice(),
             keyboardNavigation: () => new KeyboardNavigationHandler(),
             inputManager: new InputManager(),

+ 0 - 1
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -1347,7 +1347,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             return UnitTestApplication.Start(TestServices.StyledWindow.With(
                 windowingPlatform: CreateMockWindowingPlatform(),
-                focusManager: new FocusManager(),
                 keyboardDevice: () => new KeyboardDevice(),
                 keyboardNavigation: () => new KeyboardNavigationHandler()));
         }

+ 0 - 1
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs

@@ -1346,7 +1346,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
         {
             return UnitTestApplication.Start(
                 TestServices.MockThreadingInterface.With(
-                    focusManager: new FocusManager(),
                     fontManagerImpl: new HeadlessFontManagerStub(),
                     keyboardDevice: () => new KeyboardDevice(),
                     keyboardNavigation: () => new KeyboardNavigationHandler(),

+ 0 - 2
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -509,7 +509,6 @@ namespace Avalonia.Controls.UnitTests
         public void Tab_Navigation_Should_Move_To_First_TabItem_When_No_Anchor_Element_Selected()
         {
             var services = TestServices.StyledWindow.With(
-                focusManager: new FocusManager(),
                 keyboardDevice: () => new KeyboardDevice());
             using var app = UnitTestApplication.Start(services);
 
@@ -558,7 +557,6 @@ namespace Avalonia.Controls.UnitTests
         public void Tab_Navigation_Should_Move_To_Anchor_TabItem()
         {
             var services = TestServices.StyledWindow.With(
-                focusManager: new FocusManager(),
                 keyboardDevice: () => new KeyboardDevice());
             using var app = UnitTestApplication.Start(services);
 

+ 0 - 1
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -2130,7 +2130,6 @@ namespace Avalonia.Controls.UnitTests
         }
 
         private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
-            focusManager: new FocusManager(),
             keyboardDevice: () => new KeyboardDevice(),
             keyboardNavigation: () => new KeyboardNavigationHandler(),
             inputManager: new InputManager(),

+ 0 - 1
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -1836,7 +1836,6 @@ namespace Avalonia.Controls.UnitTests
         {
             return UnitTestApplication.Start(
                 TestServices.MockThreadingInterface.With(
-                    focusManager: new FocusManager(),
                     fontManagerImpl: new HeadlessFontManagerStub(),
                     keyboardDevice: () => new KeyboardDevice(),
                     keyboardNavigation: () => new KeyboardNavigationHandler(),

+ 0 - 1
tests/Avalonia.LeakTests/ControlTests.cs

@@ -1027,7 +1027,6 @@ namespace Avalonia.LeakTests
             {
                 Disposable.Create(Cleanup),
                 UnitTestApplication.Start(TestServices.StyledWindow.With(
-                    focusManager: new FocusManager(),
                     keyboardDevice: () => new KeyboardDevice(),
                     inputManager: new InputManager()))
             };

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Delay.cs

@@ -25,7 +25,7 @@ public class BindingTests_Delay : ScopedTestBase, IDisposable
     public BindingTests_Delay()
     {
         _dispatcher = new ManualTimerDispatcher();
-        _app = UnitTestApplication.Start(new(dispatcherImpl: _dispatcher, focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice()));
+        _app = UnitTestApplication.Start(new(dispatcherImpl: _dispatcher, keyboardDevice: () => new KeyboardDevice()));
 
         _source = new BindingTests.Source { Foo = InitialFooValue };
         _target = new TextBox { DataContext = _source };

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

@@ -15,6 +15,7 @@ namespace Avalonia.UnitTests
     public class TestRoot : Decorator, IFocusScope, ILayoutRoot, IInputRoot, IRenderRoot, IStyleHost, ILogicalRoot
     {
         private readonly NameScope _nameScope = new NameScope();
+        private FocusManager? _focusManager;
 
         public TestRoot()
         {
@@ -63,7 +64,7 @@ namespace Avalonia.UnitTests
         IHitTester IRenderRoot.HitTester => HitTester;
 
         public IKeyboardNavigationHandler KeyboardNavigationHandler => null;
-        public IFocusManager FocusManager => AvaloniaLocator.Current.GetService<IFocusManager>();
+        public IFocusManager FocusManager => _focusManager ??= new FocusManager(this);
         public IPlatformSettings PlatformSettings => AvaloniaLocator.Current.GetService<IPlatformSettings>();
 
         public IInputElement PointerOverElement { get; set; }

+ 1 - 9
tests/Avalonia.UnitTests/TestServices.cs

@@ -41,7 +41,6 @@ namespace Avalonia.UnitTests
             windowingPlatform: new MockWindowingPlatform());
 
         public static readonly TestServices RealFocus = new TestServices(
-            focusManager: new FocusManager(),
             keyboardDevice: () => new KeyboardDevice(),
             keyboardNavigation: () => new KeyboardNavigationHandler(),
             inputManager: new InputManager(),
@@ -51,7 +50,6 @@ namespace Avalonia.UnitTests
             textShaperImpl: new HeadlessTextShaperStub());
 
         public static readonly TestServices FocusableWindow = new TestServices(
-            focusManager: new FocusManager(),
             keyboardDevice: () => new KeyboardDevice(),
             keyboardNavigation: () => new KeyboardNavigationHandler(),
             inputManager: new InputManager(),
@@ -73,7 +71,6 @@ namespace Avalonia.UnitTests
         
         public TestServices(
             IAssetLoader assetLoader = null,
-            IFocusManager focusManager = null,
             IInputManager inputManager = null,
             Func<IKeyboardDevice> keyboardDevice = null,
             Func<IKeyboardNavigationHandler> keyboardNavigation = null,
@@ -90,7 +87,6 @@ namespace Avalonia.UnitTests
             IWindowingPlatform windowingPlatform = null)
         {
             AssetLoader = assetLoader;
-            FocusManager = focusManager;
             InputManager = inputManager;
             KeyboardDevice = keyboardDevice;
             KeyboardNavigation = keyboardNavigation;
@@ -109,7 +105,6 @@ namespace Avalonia.UnitTests
         internal TestServices(
             IGlobalClock globalClock,
             IAssetLoader assetLoader = null,
-            IFocusManager focusManager = null,
             IInputManager inputManager = null,
             Func<IKeyboardDevice> keyboardDevice = null,
             Func<IKeyboardNavigationHandler> keyboardNavigation = null,
@@ -125,7 +120,7 @@ namespace Avalonia.UnitTests
             IWindowImpl windowImpl = null,
             IWindowingPlatform windowingPlatform = null,
             IAccessKeyHandler accessKeyHandler = null
-            ) : this(assetLoader, focusManager, inputManager, keyboardDevice,
+            ) : this(assetLoader, inputManager, keyboardDevice,
             keyboardNavigation,
             mouseDevice, platform, renderInterface, renderLoop, standardCursorFactory, theme,
             dispatcherImpl, fontManagerImpl, textShaperImpl, windowImpl, windowingPlatform)
@@ -136,7 +131,6 @@ namespace Avalonia.UnitTests
 
         public IAssetLoader AssetLoader { get; }
         public IInputManager InputManager { get; }
-        public IFocusManager FocusManager { get; }
         internal IGlobalClock GlobalClock { get; set; }
         internal IAccessKeyHandler AccessKeyHandler { get; }
         public Func<IKeyboardDevice> KeyboardDevice { get; }
@@ -154,7 +148,6 @@ namespace Avalonia.UnitTests
 
         internal TestServices With(
             IAssetLoader assetLoader = null,
-            IFocusManager focusManager = null,
             IInputManager inputManager = null,
             Func<IKeyboardDevice> keyboardDevice = null,
             Func<IKeyboardNavigationHandler> keyboardNavigation = null,
@@ -176,7 +169,6 @@ namespace Avalonia.UnitTests
             return new TestServices(
                 globalClock ?? GlobalClock,
                 assetLoader: assetLoader ?? AssetLoader,
-                focusManager: focusManager ?? FocusManager,
                 inputManager: inputManager ?? InputManager,
                 keyboardDevice: keyboardDevice ?? KeyboardDevice,
                 keyboardNavigation: keyboardNavigation ?? KeyboardNavigation,

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

@@ -63,7 +63,6 @@ namespace Avalonia.UnitTests
         {
             AvaloniaLocator.CurrentMutable
                 .Bind<IAssetLoader>().ToConstant(Services.AssetLoader)
-                .Bind<IFocusManager>().ToConstant(Services.FocusManager)
                 .Bind<IGlobalClock>().ToConstant(Services.GlobalClock)
                 .BindToSelf<IGlobalStyles>(this)
                 .Bind<IInputManager>().ToConstant(Services.InputManager)