Browse Source

Fix Mouse pointer triggering focus change on pointer release. (#21009)

* add tests

* explicitly check which pointer events trigger focus change

* add test checking for change in focus while mouse is pressed.
pull/21048/head
Emmanuel Hansen 6 months ago
committed by GitHub
parent
commit
118a1174e2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 14
      src/Avalonia.Base/Input/FocusManager.cs
  2. 50
      tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
  3. 103
      tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs
  4. 2
      tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs
  5. 67
      tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

14
src/Avalonia.Base/Input/FocusManager.cs

@ -111,7 +111,7 @@ namespace Avalonia.Input
scope.ClearValue(FocusedElementProperty);
}
if (Current == removedElement)
if (Current == removedElement)
Focus(null);
}
@ -158,7 +158,7 @@ namespace Avalonia.Input
/// </summary>
internal static FocusManager? GetFocusManager(IInputElement? element)
{
// Element might not be a visual, and not attached to the root.
// But IFocusManager is always expected to be a FocusManager.
return (FocusManager?)(element as Visual)?.GetInputRoot()?.FocusManager
@ -188,8 +188,12 @@ namespace Avalonia.Input
{
if (CanFocus(e))
{
if (ev.Pointer.Type == PointerType.Mouse || ev is PointerReleasedEventArgs)
return true;
return ev switch
{
PointerReleasedEventArgs releasedEventArgs when releasedEventArgs.Pointer.Type != PointerType.Mouse => true,
PointerPressedEventArgs pressedEventArgs when pressedEventArgs.Pointer.Type == PointerType.Mouse => true,
_ => false,
};
}
return false;
@ -229,7 +233,7 @@ namespace Avalonia.Input
var root = v.PresentationSource?.InputRoot.FocusRoot as Visual;
while (root is IHostedVisualTreeRoot hosted &&
hosted.Host?.PresentationSource?.InputRoot.FocusRoot is {} parentRoot)
hosted.Host?.PresentationSource?.InputRoot.FocusRoot is { } parentRoot)
{
root = parentRoot;
}

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

@ -24,7 +24,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Same(target, root.FocusManager.GetFocusedElement());
}
}
[Fact]
public void Invisible_Controls_Should_Not_Receive_Focus()
{
@ -34,20 +34,20 @@ namespace Avalonia.Base.UnitTests.Input
{
var root = new TestRoot
{
Child = target = new Button() { IsVisible = false}
Child = target = new Button() { IsVisible = false }
};
Assert.Null(root.FocusManager.GetFocusedElement());
target.Focus();
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
Assert.Null(root.FocusManager.GetFocusedElement());
}
}
[Fact]
public void Effectively_Invisible_Controls_Should_Not_Receive_Focus()
{
@ -64,11 +64,11 @@ namespace Avalonia.Base.UnitTests.Input
Children = { target }
}
};
Assert.Null(root.FocusManager.GetFocusedElement());
target.Focus();
Assert.False(target.IsFocused);
Assert.False(target.IsKeyboardFocusWithin);
@ -87,7 +87,7 @@ namespace Avalonia.Base.UnitTests.Input
var root = new TestRoot
{
Child = new StackPanel
{
{
Children =
{
(first = new Button()),
@ -365,7 +365,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(target2.Classes.Contains(":focus-visible"));
}
}
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied()
{
@ -398,7 +398,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_PseudoClass_Should_Be_Applied_and_Removed()
{
@ -419,7 +419,7 @@ namespace Avalonia.Base.UnitTests.Input
}
}
};
target1.ApplyTemplate();
target2.ApplyTemplate();
@ -433,9 +433,9 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
target2.Focus();
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
@ -445,7 +445,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root.Child.IsKeyboardFocusWithin);
Assert.True(root.Classes.Contains(":focus-within"));
Assert.True(root.IsKeyboardFocusWithin);
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
@ -453,7 +453,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(panel2.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_When_Removed_From_Tree()
{
@ -487,11 +487,11 @@ namespace Avalonia.Base.UnitTests.Input
var keyboardDevice = KeyboardDevice.Instance!;
Assert.Equal(keyboardDevice.FocusedElement, target1);
root.Child = null;
Assert.Null(keyboardDevice.FocusedElement);
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
@ -499,7 +499,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(root.IsKeyboardFocusWithin);
}
}
[Fact]
public void Control_FocusWithin_Pseudoclass_Should_Be_Removed_Focus_Moves_To_Different_Root()
{
@ -507,7 +507,7 @@ namespace Avalonia.Base.UnitTests.Input
{
var target1 = new Decorator { Focusable = true };
var target2 = new Decorator { Focusable = true };
var root1 = new TestRoot
{
Child = new StackPanel
@ -518,7 +518,7 @@ namespace Avalonia.Base.UnitTests.Input
}
}
};
var root2 = new TestRoot
{
Child = new StackPanel
@ -543,9 +543,9 @@ namespace Avalonia.Base.UnitTests.Input
Assert.True(root1.IsKeyboardFocusWithin);
Assert.Equal(KeyboardDevice.Instance!.FocusedElement, target1);
target2.Focus();
Assert.False(target1.IsFocused);
Assert.False(target1.Classes.Contains(":focus-within"));
Assert.False(target1.IsKeyboardFocusWithin);
@ -553,7 +553,7 @@ namespace Avalonia.Base.UnitTests.Input
Assert.False(root1.Child.IsKeyboardFocusWithin);
Assert.False(root1.Classes.Contains(":focus-within"));
Assert.False(root1.IsKeyboardFocusWithin);
Assert.True(target2.IsFocused);
Assert.True(target2.Classes.Contains(":focus-within"));
Assert.True(target2.IsKeyboardFocusWithin);
@ -1031,7 +1031,7 @@ namespace Avalonia.Base.UnitTests.Input
[XYFocus.UpProperty] = target3,
[XYFocus.DownProperty] = target4,
};
var container = new Canvas
var container = new Canvas
{
Children =
{

103
tests/Avalonia.Base.UnitTests/Input/MouseDeviceTests.cs

@ -4,7 +4,6 @@ using Avalonia.Input.Raw;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -18,7 +17,7 @@ namespace Avalonia.Base.UnitTests.Input
{
using var scope = AvaloniaLocator.EnterScope();
var settingsMock = new Mock<IPlatformSettings>();
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(settingsMock.Object);
@ -32,7 +31,7 @@ namespace Avalonia.Base.UnitTests.Input
var control = new Control();
var root = CreateInputRoot(impl.Object, control, renderer.Object);
MouseButton button = default;
root.PointerReleased += (s, e) => button = e.InitialPressMouseButton;
@ -50,10 +49,10 @@ namespace Avalonia.Base.UnitTests.Input
impl.Object.Input!(up);
Assert.Equal(MouseButton.Left, button);
impl.Object.Input!(up);
Assert.Equal(MouseButton.None, button);
Assert.Equal(MouseButton.None, button);
}
[Fact]
@ -85,7 +84,7 @@ namespace Avalonia.Base.UnitTests.Input
impl.Object.Input!(CreateRawPointerMovedArgs(device, root));
Assert.NotNull(result);
result.Capture(control);
Assert.Same(control, result.Captured);
@ -115,8 +114,8 @@ namespace Avalonia.Base.UnitTests.Input
})
}
}, renderer.Object);
Point? result = null;
root.PointerMoved += (_, a) =>
{
@ -128,5 +127,93 @@ namespace Avalonia.Base.UnitTests.Input
Assert.Equal(new Point(1, 11), result);
}
[Fact]
public void Mouse_Pointer_Should_Set_Focus_On_Pointer_Pressed()
{
using var scope = AvaloniaLocator.EnterScope();
var settingsMock = new Mock<IPlatformSettings>();
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(settingsMock.Object);
using var app = UnitTestApplication.Start(
TestServices.RealFocus);
var renderer = new Mock<IHitTester>();
var impl = CreateTopLevelImplMock();
var control = new Button()
{
Focusable = true
};
var root = CreateInputRoot(impl.Object, control, renderer.Object);
var device = new MouseDevice();
var down = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown);
var up = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp);
SetHit(renderer, control);
Assert.False(control.IsFocused);
impl.Object.Input!(down);
Assert.True(control.IsFocused);
impl.Object.Input!(up);
Assert.True(control.IsFocused);
}
[Fact]
public void Control_Should_Not_Gain_Focus_On_Mouse_Release()
{
using var scope = AvaloniaLocator.EnterScope();
var settingsMock = new Mock<IPlatformSettings>();
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IPlatformSettings>().ToConstant(settingsMock.Object);
using var app = UnitTestApplication.Start(
TestServices.RealFocus);
var renderer = new Mock<IHitTester>();
var impl = CreateTopLevelImplMock();
var control1 = new Button()
{
Focusable = true
};
var control2 = new Button()
{
Focusable = true
};
var stack = new StackPanel()
{
Children = { control1, control2 }
};
var root = CreateInputRoot(impl.Object, stack, renderer.Object);
var device = new MouseDevice();
var down = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonDown);
var up = CreateRawPointerArgs(device, root, RawPointerEventType.LeftButtonUp);
SetHit(renderer, control1);
Assert.False(control1.IsFocused);
impl.Object.Input!(down);
Assert.True(control1.IsFocused);
control2.Focus();
impl.Object.Input!(up);
Assert.False(control1.IsFocused);
}
}
}

2
tests/Avalonia.Base.UnitTests/Input/PointerTestsBase.cs

@ -7,9 +7,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
namespace Avalonia.Base.UnitTests.Input;

67
tests/Avalonia.Base.UnitTests/Input/TouchDeviceTests.cs

@ -1,14 +1,16 @@
using System;
using Avalonia.Base.UnitTests.Input;
using Avalonia.Controls;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class TouchDeviceTests
public class TouchDeviceTests : PointerTestsBase
{
[Fact]
public void Tapped_Event_Is_Fired_With_Touch()
@ -141,6 +143,36 @@ namespace Avalonia.Input.UnitTests
Assert.Equal(0, doubleTappedExecutedTimes);
}
[Fact]
public void Touch_Pointer_Should_Set_Focus_On_Pointer_Released()
{
using var scope = AvaloniaLocator.EnterScope();
using var app = UnitTestApplication.Start(
TestServices.RealFocus);
var impl = CreateTopLevelImplMock();
var renderer = new Mock<IHitTester>();
var root = new TestTopLevel(impl.Object)
{
HitTesterOverride = renderer.Object,
};
var host = root.TopLevelHost;
host.Focusable = true;
var touchDevice = new TouchDevice();
var inputManager = InputManager.Instance!;
Assert.False(host.IsFocused);
Press(InputManager.Instance!, touchDevice, root.InputRoot);
Assert.False(host.IsFocused);
Release(InputManager.Instance!, touchDevice, root.InputRoot);
Assert.True(host.IsFocused);
}
[Fact]
public void Click_Counting_Should_Work_Correctly_With_Few_Touch_Contacts()
{
@ -241,14 +273,12 @@ namespace Avalonia.Input.UnitTests
private static void TapOnce(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
{
inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
root,
RawPointerEventType.TouchBegin,
new Point(0, 0),
RawInputModifiers.None)
{
RawPointerId = touchPointId
});
Press(inputManager, device, root, timestamp, touchPointId);
Release(inputManager, device, root, timestamp, touchPointId);
}
private static void Release(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
{
inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
root,
RawPointerEventType.TouchEnd,
@ -258,5 +288,22 @@ namespace Avalonia.Input.UnitTests
RawPointerId = touchPointId
});
}
private static void Press(IInputManager inputManager, TouchDevice device, IInputRoot root, ulong timestamp = 0, long touchPointId = 0)
{
inputManager.ProcessInput(new RawPointerEventArgs(device, timestamp,
root,
RawPointerEventType.TouchBegin,
new Point(0, 0),
RawInputModifiers.None)
{
RawPointerId = touchPointId
});
}
private class TestTopLevel(ITopLevelImpl impl) : TopLevel(impl)
{
}
}
}

Loading…
Cancel
Save