From 7530b74cdb9007113ad136cbc859fb686d15e6b8 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 31 Mar 2026 20:39:46 +0200 Subject: [PATCH] Fix FocusManager.FocusedElement on canceled/redirected focus (#21047) * Add failing tests for FocusManager.FocusedElement * Fix FocusManager.FocusedElement on canceled/redirected focus * Fix IFocusManager documentation * Add new focus restoration test --- src/Avalonia.Base/Input/FocusManager.cs | 50 ++++--- src/Avalonia.Base/Input/IFocusManager.cs | 4 +- .../Input/InputElement_Focus.cs | 123 ++++++++++++++++++ 3 files changed, 157 insertions(+), 20 deletions(-) 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 { }