diff --git a/src/Avalonia.Base/Input/FocusManager.cs b/src/Avalonia.Base/Input/FocusManager.cs index 2bf666af44..c8de7267ca 100644 --- a/src/Avalonia.Base/Input/FocusManager.cs +++ b/src/Avalonia.Base/Input/FocusManager.cs @@ -122,6 +122,11 @@ namespace Avalonia.Input { scope = scope ?? throw new ArgumentNullException(nameof(scope)); + if (element is not null && !CanFocus(element)) + { + return; + } + if (_focusScopes.TryGetValue(scope, out var existingElement)) { if (element != existingElement) @@ -242,6 +247,6 @@ namespace Avalonia.Input } } - private static bool IsVisible(IInputElement e) => (e as Visual)?.IsVisible ?? true; + private static bool IsVisible(IInputElement e) => (e as Visual)?.IsEffectivelyVisible ?? true; } } diff --git a/src/Avalonia.Base/Input/InputElement.cs b/src/Avalonia.Base/Input/InputElement.cs index 962c7aa334..33ddbaedf9 100644 --- a/src/Avalonia.Base/Input/InputElement.cs +++ b/src/Avalonia.Base/Input/InputElement.cs @@ -647,6 +647,10 @@ namespace Avalonia.Input { PseudoClasses.Set(":focus-within", change.GetNewValue()); } + else if (change.Property == IsVisibleProperty && !change.GetNewValue() && IsFocused) + { + FocusManager.Instance?.Focus(null); + } } /// diff --git a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs index e36ce21009..ac1547d09f 100644 --- a/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs +++ b/tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs @@ -26,6 +26,138 @@ namespace Avalonia.Base.UnitTests.Input Assert.Same(target, FocusManager.Instance.Current); } } + + [Fact] + public void Invisible_Controls_Should_Not_Receive_Focus() + { + Button target; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = target = new Button() { IsVisible = false} + }; + + Assert.Null(FocusManager.Instance.Current); + + target.Focus(); + + Assert.False(target.IsFocused); + Assert.False(target.IsKeyboardFocusWithin); + + Assert.Null(FocusManager.Instance.Current); + } + } + + [Fact] + public void Effectively_Invisible_Controls_Should_Not_Receive_Focus() + { + var target = new Button(); + Panel container; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = container = new Panel + { + IsVisible = false, + Children = { target } + } + }; + + Assert.Null(FocusManager.Instance.Current); + + target.Focus(); + + Assert.False(target.IsFocused); + Assert.False(target.IsKeyboardFocusWithin); + + Assert.Null(FocusManager.Instance.Current); + } + } + + [Fact] + public void Trying_To_Focus_Invisible_Control_Should_Not_Change_Focus() + { + Button first; + Button second; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = new StackPanel + { + Children = + { + (first = new Button()), + (second = new Button() { IsVisible = false}), + } + } + }; + + first.Focus(); + + Assert.Same(first, FocusManager.Instance.Current); + + second.Focus(); + + Assert.Same(first, FocusManager.Instance.Current); + } + } + + [Fact] + public void Disabled_Controls_Should_Not_Receive_Focus() + { + Button target; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = target = new Button() { IsEnabled = false } + }; + + Assert.Null(FocusManager.Instance.Current); + + target.Focus(); + + Assert.False(target.IsFocused); + Assert.False(target.IsKeyboardFocusWithin); + + Assert.Null(FocusManager.Instance.Current); + } + } + + [Fact] + public void Effectively_Disabled_Controls_Should_Not_Receive_Focus() + { + var target = new Button(); + Panel container; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = container = new Panel + { + IsEnabled = false, + Children = { target } + } + }; + + Assert.Null(FocusManager.Instance.Current); + + target.Focus(); + + Assert.False(target.IsFocused); + Assert.False(target.IsKeyboardFocusWithin); + + Assert.Null(FocusManager.Instance.Current); + } + } [Fact] public void Focus_Should_Not_Get_Restored_To_Enabled_Control() @@ -54,6 +186,90 @@ namespace Avalonia.Base.UnitTests.Input } } + [Fact] + public void Focus_Should_Be_Cleared_When_Control_Is_Hidden() + { + Button target; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = target = new Button() + }; + + target.Focus(); + target.IsVisible = false; + + Assert.Null(FocusManager.Instance.Current); + } + } + + [Fact(Skip = "Need to implement IsEffectivelyVisible change notifications.")] + public void Focus_Should_Be_Cleared_When_Control_Is_Effectively_Hidden() + { + Border container; + Button target; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = container = new Border + { + Child = target = new Button(), + } + }; + + target.Focus(); + container.IsVisible = false; + + Assert.Null(FocusManager.Instance.Current); + } + } + + [Fact] + public void Focus_Should_Be_Cleared_When_Control_Is_Disabled() + { + Button target; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = target = new Button() + }; + + target.Focus(); + target.IsEnabled = false; + + Assert.Null(FocusManager.Instance.Current); + } + } + + [Fact] + public void Focus_Should_Be_Cleared_When_Control_Is_Effectively_Disabled() + { + Border container; + Button target; + + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var root = new TestRoot + { + Child = container = new Border + { + Child = target = new Button(), + } + }; + + target.Focus(); + container.IsEnabled = false; + + Assert.Null(FocusManager.Instance.Current); + } + } + [Fact] public void Focus_Should_Be_Cleared_When_Control_Is_Removed_From_VisualTree() { @@ -78,8 +294,8 @@ namespace Avalonia.Base.UnitTests.Input { using (UnitTestApplication.Start(TestServices.RealFocus)) { - var target1 = new Decorator(); - var target2 = new Decorator(); + var target1 = new Decorator { Focusable = true }; + var target2 = new Decorator { Focusable = true }; var root = new TestRoot { Child = new StackPanel @@ -115,8 +331,8 @@ namespace Avalonia.Base.UnitTests.Input { using (UnitTestApplication.Start(TestServices.RealFocus)) { - var target1 = new Decorator(); - var target2 = new Decorator(); + var target1 = new Decorator { Focusable = true }; + var target2 = new Decorator { Focusable = true }; var root = new TestRoot { Child = new StackPanel @@ -157,8 +373,8 @@ namespace Avalonia.Base.UnitTests.Input { using (UnitTestApplication.Start(TestServices.RealFocus)) { - var target1 = new Decorator(); - var target2 = new Decorator(); + var target1 = new Decorator { Focusable = true }; + var target2 = new Decorator { Focusable = true }; var root = new TestRoot { Child = new StackPanel @@ -190,8 +406,8 @@ namespace Avalonia.Base.UnitTests.Input { using (UnitTestApplication.Start(TestServices.RealFocus)) { - var target1 = new Decorator(); - var target2 = new Decorator(); + var target1 = new Decorator { Focusable = true }; + var target2 = new Decorator { Focusable = true }; var panel1 = new Panel { Children = { target1 } }; var panel2 = new Panel { Children = { target2 } }; var root = new TestRoot @@ -245,8 +461,8 @@ namespace Avalonia.Base.UnitTests.Input { using (UnitTestApplication.Start(TestServices.RealFocus)) { - var target1 = new Decorator(); - var target2 = new Decorator(); + var target1 = new Decorator { Focusable = true }; + var target2 = new Decorator { Focusable = true }; var root = new TestRoot { Child = new StackPanel @@ -290,8 +506,8 @@ namespace Avalonia.Base.UnitTests.Input { using (UnitTestApplication.Start(TestServices.RealFocus)) { - var target1 = new Decorator(); - var target2 = new Decorator(); + var target1 = new Decorator { Focusable = true }; + var target2 = new Decorator { Focusable = true }; var root1 = new TestRoot { diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 9fd56dec4a..1d1065501f 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -657,7 +657,6 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTemplate(), Text = "1234", - IsVisible = false }; var root = new TestRoot { Child = target1 }; diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index d71abe5a67..9c858a20e1 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -594,7 +594,6 @@ namespace Avalonia.Controls.UnitTests { Template = CreateTemplate(), Text = "1234", - IsVisible = false }; var root = new TestRoot { Child = target1 }; diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 951ca4e2c8..aa03a77d70 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -298,7 +298,9 @@ namespace Avalonia.Controls.UnitTests using var app = App(); var (target, scroll, itemsControl) = CreateTarget(); - target.GetRealizedElements().First()!.Focus(); + var focused = target.GetRealizedElements().First()!; + focused.Focusable = true; + focused.Focus(); Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin); scroll.Offset = new Vector(0, 200); @@ -314,6 +316,7 @@ namespace Avalonia.Controls.UnitTests var (target, scroll, itemsControl) = CreateTarget(); var focused = target.GetRealizedElements().First()!; + focused.Focusable = true; focused.Focus(); Assert.True(focused.IsKeyboardFocusWithin); @@ -331,6 +334,7 @@ namespace Avalonia.Controls.UnitTests var (target, scroll, itemsControl) = CreateTarget(); var focused = target.GetRealizedElements().First()!; + focused.Focusable = true; focused.Focus(); Assert.True(focused.IsKeyboardFocusWithin); @@ -350,12 +354,14 @@ namespace Avalonia.Controls.UnitTests var (target, scroll, itemsControl) = CreateTarget(); var originalFocused = target.GetRealizedElements().First()!; + originalFocused.Focusable = true; originalFocused.Focus(); scroll.Offset = new Vector(0, 500); Layout(target); var newFocused = target.GetRealizedElements().First()!; + newFocused.Focusable = true; newFocused.Focus(); Assert.False(originalFocused.IsVisible); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 55825561af..d6318bba0b 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -558,7 +558,7 @@ namespace Avalonia.LeakTests control.ContextMenu = null; } - var window = new Window(); + var window = new Window { Focusable = true }; window.Show(); Assert.Same(window, FocusManager.Instance.Current); @@ -605,7 +605,7 @@ namespace Avalonia.LeakTests contextMenu.Close(); } - var window = new Window(); + var window = new Window { Focusable = true }; window.Show(); Assert.Same(window, FocusManager.Instance.Current);