Skip to content

Commit 7221887

Browse files
authored
Fix focus being locked when restoring focus to a control thats detached from visual tree. (#22113)
* add failing test for detached focus * Clear focus if restore element is not in focus scope * check for focus eligibility when restoring focus * add failing test for second focus scope stealing focus * if focus scope is not the current focus scope, don't attempt to set focus
1 parent 0820644 commit 7221887

4 files changed

Lines changed: 130 additions & 3 deletions

File tree

src/Avalonia.Base/Input/FocusManager.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,22 @@ public bool Focus(
8080

8181
if (_focusRoot?.GetValue(FocusedElementProperty) is { } restore && restore != Current)
8282
{
83-
return FocusCore(keyboardDevice, restore, method, keyModifiers);
83+
if (!CanFocus(restore))
84+
{
85+
// Previous effective focus is no longer part of the focus root's visual tree. We clear the focused element
86+
_focusRoot.ClearValue(FocusedElementProperty);
87+
88+
if (Current != null && GetFocusScope(Current) != _focusRoot)
89+
{
90+
_focusRoot = null;
91+
92+
return false;
93+
}
94+
}
95+
else
96+
{
97+
return FocusCore(keyboardDevice, restore, method, keyModifiers);
98+
}
8499
}
85100

86101
_focusRoot = null;

src/Avalonia.Base/Input/InputElement.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,8 +568,8 @@ protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventAr
568568
if (IsFocused)
569569
{
570570
var root = e.AttachmentPoint ?? e.RootVisual;
571-
((FocusManager?)e.PresentationSource.InputRoot.FocusManager)
572-
?.ClearFocusOnElementRemoved(this, root);
571+
(((FocusManager?)e.PresentationSource.InputRoot.FocusManager) ??
572+
FocusManager.GetFocusManager(this))?.ClearFocusOnElementRemoved(this, root);
573573
}
574574

575575
IsKeyboardFocusWithin = false;

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

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
using System.Linq;
2+
using Avalonia.Collections;
13
using Avalonia.Controls;
4+
using Avalonia.Controls.Templates;
25
using Avalonia.Input;
36
using Avalonia.UnitTests;
47
using Xunit;
@@ -1629,6 +1632,114 @@ public void Focus_Should_Return_To_First_Window_When_Second_Is_Closed()
16291632
Assert.Same(first, window1.FocusManager.GetFocusedElement());
16301633
}
16311634

1635+
[Fact]
1636+
public void Focus_Should_Stay_In_Active_Window()
1637+
{
1638+
using var app = UnitTestApplication.Start(
1639+
TestServices.StyledWindow.With(keyboardDevice: () => new KeyboardDevice()));
1640+
var first = new Button { Name = "FirstButton" };
1641+
var second = new Button { Name = "SecondButton" };
1642+
1643+
var window1 = new Window
1644+
{
1645+
Content = first
1646+
};
1647+
1648+
var window2 = new Window
1649+
{
1650+
Content = second
1651+
};
1652+
1653+
window1.Show();
1654+
1655+
// Focus the first button in the first window
1656+
first.Focus();
1657+
Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
1658+
Assert.Same(first, window1.FocusManager.GetFocusedElement());
1659+
1660+
window2.Show();
1661+
1662+
// Focus the second button in the second window
1663+
second.Focus();
1664+
Assert.Same(second, KeyboardDevice.Instance?.FocusedElement);
1665+
Assert.Same(second, window2.FocusManager.GetFocusedElement());
1666+
1667+
// Activate the first window again
1668+
window1.PlatformImpl?.Activated?.Invoke();
1669+
1670+
// Focus should have moved back to the first button in the first window
1671+
Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
1672+
Assert.Same(first, window1.FocusManager.GetFocusedElement());
1673+
1674+
// Close the second window
1675+
window2.Close();
1676+
1677+
// Focus should still be in the first window
1678+
Assert.Same(first, KeyboardDevice.Instance?.FocusedElement);
1679+
Assert.Same(first, window1.FocusManager.GetFocusedElement());
1680+
}
1681+
1682+
[Fact]
1683+
public void Focus_Should_Not_Be_Restored_To_Detached_Control()
1684+
{
1685+
using var app = UnitTestApplication.Start(
1686+
TestServices.StyledWindow.With(keyboardDevice: () => new KeyboardDevice()));
1687+
var button = new Button { Name = "Button" };
1688+
MenuItem topLevelMenu;
1689+
MenuItem childMenu;
1690+
var menu = new Menu
1691+
{
1692+
Items =
1693+
{
1694+
(topLevelMenu = new MenuItem
1695+
{
1696+
Header = "Foo",
1697+
Items =
1698+
{
1699+
(childMenu = new MenuItem { Header = "Bar" })
1700+
}
1701+
}),
1702+
}
1703+
};
1704+
var panel = new StackPanel();
1705+
panel.Children.Add(button);
1706+
panel.Children.Add(menu);
1707+
1708+
var window = new Window
1709+
{
1710+
Content = panel,
1711+
Name = "Window1"
1712+
};
1713+
1714+
window.Show();
1715+
1716+
// Focus the button
1717+
button.Focus();
1718+
Assert.Same(button, KeyboardDevice.Instance?.FocusedElement);
1719+
Assert.Same(button, window.FocusManager.GetFocusedElement());
1720+
1721+
1722+
// Open the menu and focus the child menu
1723+
menu.Open();
1724+
topLevelMenu.IsSubMenuOpen = true;
1725+
childMenu.Focus();
1726+
1727+
// Remove the previously focused button.
1728+
panel.Children.Remove(button);
1729+
1730+
1731+
// Close the menus.
1732+
menu.Close();
1733+
topLevelMenu.Close();
1734+
1735+
window.PlatformImpl?.Activated?.Invoke();
1736+
1737+
// When window is activated, focus should be empty
1738+
Assert.Same(null, KeyboardDevice.Instance?.FocusedElement);
1739+
Assert.Same(null, window.FocusManager.GetFocusedElement());
1740+
}
1741+
1742+
16321743
private class TestFocusScope : Panel, IFocusScope
16331744
{
16341745
}

tests/Avalonia.UnitTests/MockWindowingPlatform.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static Mock<IWindowImpl> CreateWindowMock(double initialWidth = 800, doub
4444

4545
windowImpl.Setup(x => x.Dispose()).Callback(() =>
4646
{
47+
windowImpl.Object.LostFocus?.Invoke();
4748
windowImpl.Object.Closed?.Invoke();
4849
});
4950

0 commit comments

Comments
 (0)