Browse Source

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
release/12.0.0-rc2
Julien Lebosquain 6 months ago
committed by GitHub
parent
commit
7530b74cdb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 50
      src/Avalonia.Base/Input/FocusManager.cs
  2. 4
      src/Avalonia.Base/Input/IFocusManager.cs
  3. 123
      tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs

50
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)

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

@ -24,8 +24,8 @@ namespace Avalonia.Input
/// 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.
/// When this method returns <c>true</c>, the focus has been moved to <paramref name="element"/>.
/// When this method returns <c>false</c>, the focus may have been canceled or redirected to another element.
/// </remarks>
bool Focus(
IInputElement? element,

123
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
{
}

Loading…
Cancel
Save