diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs
index 651210fc2b..72a13d385d 100644
--- a/src/Avalonia.Base/Input/FocusManager.cs
+++ b/src/Avalonia.Base/Input/FocusManager.cs
@@ -75,30 +75,44 @@ namespace Avalonia.Input
if (element is not null)
{
- if (!CanFocus(element))
- return false;
+ return FocusCore(keyboardDevice, element, method, keyModifiers);
+ }
+
+ if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore && restore != Current)
+ {
+ return FocusCore(keyboardDevice, restore, method, keyModifiers);
+ }
+
+ _focusRoot = null;
+ keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
+ return false;
+ }
+
+ private bool FocusCore(
+ KeyboardDevice keyboardDevice,
+ IInputElement element,
+ NavigationMethod method,
+ KeyModifiers keyModifiers)
+ {
+ if (!CanFocus(element))
+ return false;
+
+ keyboardDevice.SetFocusedElement(element, method, keyModifiers);
- if (GetFocusScope(element) is StyledElement scope)
+ if (keyboardDevice.FocusedElement is { } effectivelyFocusedElement)
+ {
+ if (GetFocusScope(effectivelyFocusedElement) is { } scope)
{
- scope.SetValue(FocusedElementProperty, element);
+ scope.SetValue(FocusedElementProperty, effectivelyFocusedElement);
_focusRoot = GetFocusRoot(scope);
}
- keyboardDevice.SetFocusedElement(element, method, keyModifiers);
- return true;
- }
- else if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore &&
- restore != Current &&
- Focus(restore))
- {
- return true;
- }
- else
- {
- _focusRoot = null;
- keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
- return false;
+ return effectivelyFocusedElement == element;
}
+
+ _focusRoot = null;
+ keyboardDevice.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None, false);
+ return false;
}
internal void ClearFocusOnElementRemoved(IInputElement removedElement, Visual oldParent)
diff --git a/src/Avalonia.Base/Input/IFocusManager.cs b/src/Avalonia.Base/Input/IFocusManager.cs
index d9e8d36f8b..2cf51c7965 100644
--- a/src/Avalonia.Base/Input/IFocusManager.cs
+++ b/src/Avalonia.Base/Input/IFocusManager.cs
@@ -24,8 +24,8 @@ namespace Avalonia.Input
/// If 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 true, it is not guaranteed that the focus has been moved
- /// to . The focus might have been redirected to another element.
+ /// When this method returns true, the focus has been moved to .
+ /// When this method returns false, the focus may have been canceled or redirected to another element.
///
bool Focus(
IInputElement? element,
diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
index 45d665b591..9e835b2427 100644
--- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
+++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
@@ -1064,6 +1064,129 @@ namespace Avalonia.Base.UnitTests.Input
}
}
+ [Fact]
+ public void Focus_In_Scope_Should_Not_Change_When_Focus_Canceled()
+ {
+ using var app = UnitTestApplication.Start(TestServices.RealFocus);
+ var first = new Button { Name = "First" };
+ var second = new Button { Name = "Second" };
+
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ first,
+ second
+ }
+ }
+ };
+
+ var focusManager = (FocusManager)root.FocusManager;
+
+ // Focus the first element
+ first.Focus();
+ Assert.Same(first, focusManager.GetFocusedElement(root));
+
+ // Cancel focus change
+ second.GettingFocus += (_, e) => e.TryCancel();
+
+ // Move the focus to the second element: it should fail
+ var focusResult = focusManager.Focus(second);
+ Assert.False(focusResult);
+ Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
+
+ // FocusedElement for the scope should remain the same
+ var newFocusedElementInScope = focusManager.GetFocusedElement(root);
+ Assert.Same(first, newFocusedElementInScope);
+ }
+
+ [Fact]
+ public void Focus_In_Scope_Should_Match_Redirected_Element_When_Focus_Redirected()
+ {
+ using var app = UnitTestApplication.Start(TestServices.RealFocus);
+ var first = new Button { Name = "First" };
+ var second = new Button { Name = "Second" };
+ var third = new Button { Name = "Third" };
+
+ var root = new TestRoot
+ {
+ Child = new StackPanel
+ {
+ Children =
+ {
+ first,
+ second,
+ third
+ }
+ }
+ };
+
+ var focusManager = (FocusManager)root.FocusManager;
+
+ // Focus the first element
+ first.Focus();
+ Assert.Same(first, focusManager.GetFocusedElement(root));
+
+ // Redirect focus change
+ second.GettingFocus += (_, e) => e.TrySetNewFocusedElement(third);
+
+ // Move the focus to the second element: it should fail
+ var focusResult = focusManager.Focus(second);
+ Assert.False(focusResult);
+ Assert.Same(third, KeyboardDevice.Instance?.FocusedElement);
+
+ // FocusedElement for the scope should have moved to the redirected element
+ var newFocusedElementInScope = focusManager.GetFocusedElement(root);
+ Assert.Same(third, newFocusedElementInScope);
+ }
+
+ [Fact]
+ public void Focus_Should_Return_To_First_Window_When_Second_Is_Closed()
+ {
+ using var app = UnitTestApplication.Start(
+ TestServices.StyledWindow.With(keyboardDevice: () => new KeyboardDevice()));
+ var first = new Button { Name = "FirstButton" };
+ var second = new Button { Name = "SecondButton" };
+
+ var window1 = new Window
+ {
+ Content = first
+ };
+
+ var window2 = new Window
+ {
+ Content = second
+ };
+
+ window1.Show();
+
+ // Focus the first button in the first window
+ first.Focus();
+ Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
+ Assert.Same(first, window1.FocusManager.GetFocusedElement());
+
+ window2.Show();
+
+ // Focus the second button in the second window
+ second.Focus();
+ Assert.Same(second, KeyboardDevice.Instance?.FocusedElement);
+ Assert.Same(second, window2.FocusManager.GetFocusedElement());
+
+ // Close the second window, focus should be lost
+ window2.Close();
+ Assert.Null(KeyboardDevice.Instance?.FocusedElement);
+ Assert.Null(window2.FocusManager.GetFocusedElement());
+
+ // Activate the first window again
+ window1.PlatformImpl?.Activated?.Invoke();
+
+ // Focus should have moved back to the first button in the first window
+ Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
+ Assert.Same(first, window1.FocusManager.GetFocusedElement());
+ }
+
private class TestFocusScope : Panel, IFocusScope
{
}