Browse Source

Open FocusManager API (#20854)

* Open FocusManager API

* Merge some FocusManager overloads

* Update API suppressions

* Properly reset reused XYFocusOptions instances

* Clarify Focus documentation

* Improve FocusManager documentation

* Update API suppressions
pull/17825/merge
Julien Lebosquain 1 week ago
committed by GitHub
parent
commit
536daf0b4c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 132
      api/Avalonia.nupkg.xml
  2. 37
      src/Avalonia.Base/Input/FindNextElementOptions.cs
  3. 169
      src/Avalonia.Base/Input/FocusManager.cs
  4. 63
      src/Avalonia.Base/Input/IFocusManager.cs
  5. 4
      src/Avalonia.Base/Input/InputElement.cs
  6. 29
      src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs
  7. 2
      src/Avalonia.Controls/PresentationSource/PresentationSource.cs
  8. 2
      tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
  9. 4
      tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs
  10. 2
      tests/Avalonia.UnitTests/TestRoot.cs

132
api/Avalonia.nupkg.xml

@ -1117,12 +1117,48 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler</Target>
@ -2611,12 +2647,48 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.#ctor(Avalonia.Input.IInputElement)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.ClearFocusOnElementRemoved(Avalonia.Input.IInputElement,Avalonia.Visual)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.FindNextElement(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.FocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.HoldingRoutedEventArgs.#ctor(Avalonia.Input.HoldingState,Avalonia.Point,Avalonia.Input.PointerType)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.ClearFocus</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Input.IInputRoot.get_KeyboardNavigationHandler</Target>
@ -4027,6 +4099,36 @@
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindFirstFocusableElement</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindLastFocusableElement</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net10.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net10.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType})</Target>
@ -4315,6 +4417,36 @@
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindFirstFocusableElement</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindLastFocusableElement</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.FindNextElement(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.Focus(Avalonia.Input.IInputElement,Avalonia.Input.NavigationMethod,Avalonia.Input.KeyModifiers)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IFocusManager.TryMoveFocus(Avalonia.Input.NavigationDirection,Avalonia.Input.FindNextElementOptions)</Target>
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left>
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Input.IKeyboardNavigationHandler.Move(Avalonia.Input.IInputElement,Avalonia.Input.NavigationDirection,Avalonia.Input.KeyModifiers,System.Nullable{Avalonia.Input.KeyDeviceType})</Target>

37
src/Avalonia.Base/Input/FindNextElementOptions.cs

@ -6,12 +6,49 @@ using System.Threading.Tasks;
namespace Avalonia.Input
{
/// <summary>
/// Provides options to customize the behavior when identifying the next element to focus
/// during a navigation operation.
/// </summary>
public sealed class FindNextElementOptions
{
/// <summary>
/// Gets or sets the root <see cref="InputElement"/> within which the search for the next
/// focusable element will be conducted.
/// </summary>
/// <remarks>
/// This property defines the boundary for focus navigation operations. It determines the root element
/// in the visual tree under which the focusable item search is performed. If not specified, the search
/// will default to the current scope.
/// </remarks>
public InputElement? SearchRoot { get; init; }
/// <summary>
/// Gets or sets the rectangular region within the visual hierarchy that will be excluded
/// from consideration during focus navigation.
/// </summary>
public Rect ExclusionRect { get; init; }
/// <summary>
/// Gets or sets a rectangular region that serves as a hint for focus navigation.
/// This property specifies a rectangle, relative to the coordinate system of the search root,
/// which can be used as a preferred or prioritized target when navigating focus.
/// It can be null if no specific hint region is provided.
/// </summary>
public Rect? FocusHintRectangle { get; init; }
/// <summary>
/// Specifies an optional override for the navigation strategy used in XY focus navigation.
/// This property allows customizing the focus movement behavior when navigating between UI elements.
/// </summary>
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; init; }
/// <summary>
/// Specifies whether occlusivity (overlapping of elements or obstructions)
/// should be ignored during focus navigation. When set to <c>true</c>,
/// the navigation logic disregards obstructions that may block a potential
/// focus target, allowing elements behind such obstructions to be considered.
/// </summary>
public bool IgnoreOcclusivity { get; init; }
}
}

169
src/Avalonia.Base/Input/FocusManager.cs

@ -4,7 +4,6 @@ using System.Linq;
using Avalonia.Input.Navigation;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.VisualTree;
namespace Avalonia.Input
@ -12,7 +11,6 @@ namespace Avalonia.Input
/// <summary>
/// Manages focus for the application.
/// </summary>
[PrivateApi]
public class FocusManager : IFocusManager
{
/// <summary>
@ -42,58 +40,51 @@ namespace Avalonia.Input
RoutingStrategies.Tunnel);
}
[PrivateApi]
public FocusManager()
{
_contentRoot = null;
}
public FocusManager(IInputElement contentRoot)
{
_contentRoot = contentRoot;
}
internal void SetContentRoot(IInputElement? contentRoot)
/// <summary>
/// Gets or sets the content root for the focus management system.
/// </summary>
[PrivateApi]
public IInputElement? ContentRoot
{
_contentRoot = contentRoot;
get => _contentRoot;
set => _contentRoot = value;
}
private IInputElement? Current => KeyboardDevice.Instance?.FocusedElement;
private XYFocus _xyFocus = new();
private XYFocusOptions _xYFocusOptions = new XYFocusOptions();
private readonly XYFocus _xyFocus = new();
private IInputElement? _contentRoot;
private XYFocusOptions? _reusableFocusOptions;
/// <summary>
/// Gets the currently focused <see cref="IInputElement"/>.
/// </summary>
/// <inheritdoc />
public IInputElement? GetFocusedElement() => Current;
/// <summary>
/// Focuses a control.
/// </summary>
/// <param name="control">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <inheritdoc />
public bool Focus(
IInputElement? control,
IInputElement? element,
NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None)
{
if (KeyboardDevice.Instance is not { } keyboardDevice)
return false;
if (control is not null)
if (element is not null)
{
if (!CanFocus(control))
if (!CanFocus(element))
return false;
if (GetFocusScope(control) is StyledElement scope)
if (GetFocusScope(element) is StyledElement scope)
{
scope.SetValue(FocusedElementProperty, control);
scope.SetValue(FocusedElementProperty, element);
_focusRoot = GetFocusRoot(scope);
}
keyboardDevice.SetFocusedElement(control, method, keyModifiers);
keyboardDevice.SetFocusedElement(element, method, keyModifiers);
return true;
}
else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
@ -110,12 +101,7 @@ namespace Avalonia.Input
}
}
public void ClearFocus()
{
Focus(null);
}
public void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
{
if (oldParent is IInputElement parentElement &&
GetFocusScope(parentElement) is StyledElement scope &&
@ -129,6 +115,7 @@ namespace Avalonia.Input
Focus(null);
}
[PrivateApi]
public IInputElement? GetFocusedElement(IFocusScope scope)
{
return (scope as StyledElement)?.GetValue(FocusedElementProperty);
@ -138,6 +125,7 @@ namespace Avalonia.Input
/// Notifies the focus manager of a change in focus scope.
/// </summary>
/// <param name="scope">The new focus scope.</param>
[PrivateApi]
public void SetFocusScope(IFocusScope scope)
{
if (GetFocusedElement(scope) is { } focused)
@ -153,12 +141,14 @@ namespace Avalonia.Input
}
}
[PrivateApi]
public void RemoveFocusRoot(IFocusScope scope)
{
if (scope == _focusRoot)
ClearFocus();
Focus(null);
}
[PrivateApi]
public static bool GetIsFocusScope(IInputElement e) => e is IFocusScope;
/// <summary>
@ -176,25 +166,15 @@ namespace Avalonia.Input
?? (FocusManager?)AvaloniaLocator.Current.GetService<IFocusManager>();
}
/// <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)
/// <inheritdoc />
public bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null)
{
return FindAndSetNextFocus(direction, _xYFocusOptions);
}
ValidateDirection(direction);
/// <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));
var focusOptions = ToFocusOptions(options, true);
var result = FindAndSetNextFocus(direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
/// <summary>
@ -295,10 +275,7 @@ namespace Avalonia.Input
return true;
}
/// <summary>
/// Retrieves the first element that can receive focus.
/// </summary>
/// <returns>The first focusable element.</returns>
/// <inheritdoc />
public IInputElement? FindFirstFocusableElement()
{
var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
@ -317,10 +294,7 @@ namespace Avalonia.Input
return GetFirstFocusableElement(searchScope);
}
/// <summary>
/// Retrieves the last element that can receive focus.
/// </summary>
/// <returns>The last focusable element.</returns>
/// <inheritdoc />
public IInputElement? FindLastFocusableElement()
{
var root = (_contentRoot as Visual)?.GetSelfAndVisualDescendants().FirstOrDefault(x => x is IInputElement) as IInputElement;
@ -339,52 +313,59 @@ namespace Avalonia.Input
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)
/// <inheritdoc />
public IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null)
{
var xyOption = new XYFocusOptions()
{
UpdateManifold = false
};
ValidateDirection(direction);
return FindNextFocus(direction, xyOption);
var focusOptions = ToFocusOptions(options, false);
var result = FindNextFocus(direction, focusOptions);
_reusableFocusOptions = focusOptions;
return result;
}
/// <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)
private static void ValidateDirection(NavigationDirection direction)
{
return FindNextFocus(direction, ValidateAndCreateFocusOptions(direction, options));
if (direction is not (
NavigationDirection.Next or
NavigationDirection.Previous or
NavigationDirection.Up or
NavigationDirection.Down or
NavigationDirection.Left or
NavigationDirection.Right))
{
throw new ArgumentOutOfRangeException(
nameof(direction),
direction,
$"Only {nameof(NavigationDirection.Next)}, {nameof(NavigationDirection.Previous)}, " +
$"{nameof(NavigationDirection.Up)}, {nameof(NavigationDirection.Down)}," +
$" {nameof(NavigationDirection.Left)} and {nameof(NavigationDirection.Right)} directions are supported");
}
}
private static XYFocusOptions ValidateAndCreateFocusOptions(NavigationDirection direction, FindNextElementOptions options)
private XYFocusOptions ToFocusOptions(FindNextElementOptions? options, bool updateManifold)
{
if (direction is not NavigationDirection.Up
and not NavigationDirection.Down
and not NavigationDirection.Left
and not NavigationDirection.Right)
// XYFocus only uses the options and never modifies them; we can cache and reset them between calls.
var focusOptions = _reusableFocusOptions;
_reusableFocusOptions = null;
if (focusOptions is null)
focusOptions = new XYFocusOptions();
else
focusOptions.Reset();
if (options is not null)
{
throw new ArgumentOutOfRangeException(nameof(direction),
$"{direction} is not supported with FindNextElementOptions. Only Up, Down, Left and right are supported");
focusOptions.SearchRoot = options.SearchRoot;
focusOptions.ExclusionRect = options.ExclusionRect;
focusOptions.FocusHintRectangle = options.FocusHintRectangle;
focusOptions.NavigationStrategyOverride = options.NavigationStrategyOverride;
focusOptions.IgnoreOcclusivity = options.IgnoreOcclusivity;
}
return new XYFocusOptions
{
UpdateManifold = false,
SearchRoot = options.SearchRoot,
ExclusionRect = options.ExclusionRect,
FocusHintRectangle = options.FocusHintRectangle,
NavigationStrategyOverride = options.NavigationStrategyOverride,
IgnoreOcclusivity = options.IgnoreOcclusivity
};
focusOptions.UpdateManifold = updateManifold;
return focusOptions;
}
internal IInputElement? FindNextFocus(NavigationDirection direction, XYFocusOptions focusOptions, bool updateManifolds = true)

63
src/Avalonia.Base/Input/IFocusManager.cs

@ -14,9 +14,66 @@ namespace Avalonia.Input
IInputElement? GetFocusedElement();
/// <summary>
/// Clears currently focused element.
/// Focuses a control.
/// </summary>
[Unstable("This API might be removed in 11.x minor updates. Please consider focusing another element instead of removing focus at all for better UX.")]
void ClearFocus();
/// <param name="element">The control to focus.</param>
/// <param name="method">The method by which focus was changed.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
/// <returns><c>true</c> if the focus moved to a control; otherwise, <c>false</c>.</returns>
/// <remarks>
/// If <paramref name="element"/> is null, this method tries to clear the focus. However, it is not advised.
/// For a better user experience, focus should be moved to another element when possible.
///
/// When this method return <c>true</c>, it is not guaranteed that the focus has been moved
/// to <paramref name="element"/>. The focus might have been redirected to another element.
/// </remarks>
bool Focus(
IInputElement? element,
NavigationMethod method = NavigationMethod.Unspecified,
KeyModifiers keyModifiers = KeyModifiers.None);
/// <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 that focus moves from element to element.
/// Must be one of <see cref="NavigationDirection.Next"/>, <see cref="NavigationDirection.Previous"/>,
/// <see cref="NavigationDirection.Left"/>, <see cref="NavigationDirection.Right"/>,
/// <see cref="NavigationDirection.Up"/> and <see cref="NavigationDirection.Down"/>.
/// </param>
/// <param name="options">
/// The options to help identify the next element to receive focus.
/// They only apply to directional navigation.
/// </param>
/// <returns>true if focus moved; otherwise, false.</returns>
bool TryMoveFocus(NavigationDirection direction, FindNextElementOptions? options = null);
/// <summary>
/// Retrieves the first element that can receive focus.
/// </summary>
/// <returns>The first focusable element.</returns>
IInputElement? FindFirstFocusableElement();
/// <summary>
/// Retrieves the last element that can receive focus.
/// </summary>
/// <returns>The last focusable element.</returns>
IInputElement? FindLastFocusableElement();
/// <summary>
/// Retrieves the element that should receive focus based on the specified navigation direction.
/// </summary>
/// <param name="direction">
/// The direction that focus moves from element to element.
/// Must be one of <see cref="NavigationDirection.Next"/>, <see cref="NavigationDirection.Previous"/>,
/// <see cref="NavigationDirection.Left"/>, <see cref="NavigationDirection.Right"/>,
/// <see cref="NavigationDirection.Up"/> and <see cref="NavigationDirection.Down"/>.
/// </param>
/// <param name="options">
/// The options to help identify the next element to receive focus.
/// They only apply to directional navigation.
/// </param>
/// <returns>The next element to receive focus, if any.</returns>
IInputElement? FindNextElement(NavigationDirection direction, FindNextElementOptions? options = null);
}
}

4
src/Avalonia.Base/Input/InputElement.cs

@ -523,7 +523,7 @@ namespace Avalonia.Input
if (!IsEffectivelyEnabled && FocusManager.GetFocusManager(this) is { } focusManager
&& Equals(focusManager.GetFocusedElement(), this))
{
focusManager.ClearFocus();
focusManager.Focus(null);
}
}
}
@ -995,7 +995,7 @@ namespace Avalonia.Input
}
else
{
focusManager.ClearFocus();
focusManager.Focus(null);
}
}
}

29
src/Avalonia.Base/Input/Navigation/XYFocusOptions.cs

@ -1,17 +1,38 @@
namespace Avalonia.Input.Navigation;
internal class XYFocusOptions
internal sealed class XYFocusOptions
{
public InputElement? SearchRoot { get; set; }
public Rect ExclusionRect { get; set; }
public Rect? FocusHintRectangle { get; set; }
public Rect? FocusedElementBounds { get; set; }
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; set; }
public bool IgnoreClipping { get; set; } = true;
public bool IgnoreClipping { get; set; }
public bool IgnoreCone { get; set; }
public KeyDeviceType? KeyDeviceType { get; set; }
public bool ConsiderEngagement { get; set; } = true;
public bool UpdateManifold { get; set; } = true;
public bool ConsiderEngagement { get; set; }
public bool UpdateManifold { get; set; }
public bool UpdateManifoldsFromFocusHintRect { get; set; }
public bool IgnoreOcclusivity { get; set; }
public XYFocusOptions()
{
Reset();
}
internal void Reset()
{
SearchRoot = null;
ExclusionRect = default;
FocusHintRectangle = null;
FocusedElementBounds = null;
NavigationStrategyOverride = null;
IgnoreClipping = true;
IgnoreCone = false;
KeyDeviceType = null;
ConsiderEngagement = true;
UpdateManifold = true;
UpdateManifoldsFromFocusHintRect = false;
IgnoreOcclusivity = false;
}
}

2
src/Avalonia.Controls/PresentationSource/PresentationSource.cs

@ -61,7 +61,7 @@ internal partial class PresentationSource : IPresentationSource, IInputRoot, IDi
field?.SetPresentationSourceForRootVisual(this);
Renderer.CompositionTarget.Root = field?.CompositionVisual;
FocusManager.SetContentRoot(value as IInputElement);
FocusManager.ContentRoot = value;
}
}

2
tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs

@ -577,7 +577,7 @@ namespace Avalonia.Base.UnitTests.Input
};
target.Focus();
root.FocusManager.ClearFocus();
root.FocusManager.Focus(null);
Assert.Null(root.FocusManager.GetFocusedElement());
}

4
tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs

@ -15,7 +15,7 @@ namespace Avalonia.Base.UnitTests.Input
using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
var window = new Window();
window.FocusManager.ClearFocus();
window.FocusManager.Focus(null);
int raised = 0;
window.KeyDown += (sender, ev) =>
{
@ -71,7 +71,7 @@ namespace Avalonia.Base.UnitTests.Input
using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
var window = new Window();
window.FocusManager.ClearFocus();
window.FocusManager.Focus(null);
int raised = 0;
window.TextInput += (sender, ev) =>
{

2
tests/Avalonia.UnitTests/TestRoot.cs

@ -74,7 +74,7 @@ namespace Avalonia.UnitTests
IRenderer IPresentationSource.Renderer => Renderer;
IHitTester IPresentationSource.HitTester => HitTester;
public IFocusManager FocusManager => _focusManager ??= new FocusManager(this);
public IFocusManager FocusManager => _focusManager ??= new FocusManager { ContentRoot = this };
public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService<IPlatformSettings>();
public IInputElement? PointerOverElement { get; set; }

Loading…
Cancel
Save