diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index 5c6e35b567..8e0ca62131 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -78,6 +78,9 @@ namespace Avalonia.Controls
PlacementProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
}
+ internal Control? AdornedControl { get; private set; }
+ internal event EventHandler? Closed;
+
///
/// Gets the value of the ToolTip.Tip attached property.
///
@@ -214,14 +217,13 @@ namespace Avalonia.Controls
{
var control = (Control)e.Sender;
var newValue = (bool)e.NewValue!;
- ToolTip? toolTip;
if (newValue)
{
var tip = GetTip(control);
if (tip == null) return;
- toolTip = control.GetValue(ToolTipProperty);
+ var toolTip = control.GetValue(ToolTipProperty);
if (toolTip == null || (tip != toolTip && tip != toolTip.Content))
{
toolTip?.Close();
@@ -231,15 +233,16 @@ namespace Avalonia.Controls
toolTip.SetValue(ThemeVariant.RequestedThemeVariantProperty, control.ActualThemeVariant);
}
+ toolTip.AdornedControl = control;
toolTip.Open(control);
+ toolTip?.UpdatePseudoClasses(newValue);
}
- else
+ else if (control.GetValue(ToolTipProperty) is { } toolTip)
{
- toolTip = control.GetValue(ToolTipProperty);
- toolTip?.Close();
+ toolTip.AdornedControl = null;
+ toolTip.Close();
+ toolTip?.UpdatePseudoClasses(newValue);
}
-
- toolTip?.UpdatePseudoClasses(newValue);
}
private static void RecalculatePositionOnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
@@ -293,6 +296,7 @@ namespace Avalonia.Controls
_popupHost.Dispose();
_popupHost = null;
_popupHostChangedHandler?.Invoke(null);
+ Closed?.Invoke(this, EventArgs.Empty);
}
}
diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs
index 9ff4c9b256..9e75026d79 100644
--- a/src/Avalonia.Controls/ToolTipService.cs
+++ b/src/Avalonia.Controls/ToolTipService.cs
@@ -2,7 +2,6 @@ using System;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
-using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@@ -109,6 +108,11 @@ namespace Avalonia.Controls
private void ControlPointerExited(object? sender, PointerEventArgs e)
{
var control = (Control)sender!;
+
+ // If the control is showing a tooltip and the pointer is over the tooltip, don't close it.
+ if (control.GetValue(ToolTip.ToolTipProperty) is { } tooltip && tooltip.IsPointerOver)
+ return;
+
Close(control);
}
@@ -125,6 +129,27 @@ namespace Avalonia.Controls
toolTip?.RecalculatePosition(control);
}
+ private void ToolTipClosed(object? sender, EventArgs e)
+ {
+ if (sender is ToolTip toolTip)
+ {
+ toolTip.Closed -= ToolTipClosed;
+ toolTip.PointerExited -= ToolTipPointerExited;
+ }
+ }
+
+ private void ToolTipPointerExited(object? sender, PointerEventArgs e)
+ {
+ // The pointer has exited the tooltip. Close the tooltip unless the pointer is over the
+ // adorned control.
+ if (sender is ToolTip toolTip &&
+ toolTip.AdornedControl is { } control &&
+ !control.IsPointerOver)
+ {
+ Close(control);
+ }
+ }
+
private void StartShowTimer(int showDelay, Control control)
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay) };
@@ -139,6 +164,12 @@ namespace Avalonia.Controls
if (control.IsAttachedToVisualTree)
{
ToolTip.SetIsOpen(control, true);
+
+ if (control.GetValue(ToolTip.ToolTipProperty) is { } tooltip)
+ {
+ tooltip.Closed += ToolTipClosed;
+ tooltip.PointerExited += ToolTipPointerExited;
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
index 74e506105d..98a131752a 100644
--- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
@@ -293,6 +293,113 @@ namespace Avalonia.Controls.UnitTests
Assert.False(ToolTip.GetIsOpen(target));
}
}
+
+ [Fact]
+ public void Should_Not_Close_When_Pointer_Is_Moved_Over_ToolTip()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = new Window();
+
+ var target = new Decorator()
+ {
+ [ToolTip.TipProperty] = "Tip",
+ [ToolTip.ShowDelayProperty] = 0
+ };
+
+ window.Content = target;
+
+ window.ApplyStyling();
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+
+ _mouseHelper.Enter(target);
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ var tooltip = Assert.IsType(target.GetValue(ToolTip.ToolTipProperty));
+ _mouseHelper.Enter(tooltip);
+ _mouseHelper.Leave(target);
+
+ Assert.True(ToolTip.GetIsOpen(target));
+ }
+ }
+
+ [Fact]
+ public void Should_Not_Close_When_Pointer_Is_Moved_From_ToolTip_To_Original_Control()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = new Window();
+
+ var target = new Decorator()
+ {
+ [ToolTip.TipProperty] = "Tip",
+ [ToolTip.ShowDelayProperty] = 0
+ };
+
+ window.Content = target;
+
+ window.ApplyStyling();
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+
+ _mouseHelper.Enter(target);
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ var tooltip = Assert.IsType(target.GetValue(ToolTip.ToolTipProperty));
+ _mouseHelper.Enter(tooltip);
+ _mouseHelper.Leave(target);
+
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ _mouseHelper.Enter(target);
+ _mouseHelper.Leave(tooltip);
+
+ Assert.True(ToolTip.GetIsOpen(target));
+ }
+ }
+
+ [Fact]
+ public void Should_Close_When_Pointer_Is_Moved_From_ToolTip_To_Another_Control()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var window = new Window();
+
+ var target = new Decorator()
+ {
+ [ToolTip.TipProperty] = "Tip",
+ [ToolTip.ShowDelayProperty] = 0
+ };
+
+ var other = new Decorator();
+
+ var panel = new StackPanel
+ {
+ Children = { target, other }
+ };
+
+ window.Content = panel;
+
+ window.ApplyStyling();
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+
+ _mouseHelper.Enter(target);
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ var tooltip = Assert.IsType(target.GetValue(ToolTip.ToolTipProperty));
+ _mouseHelper.Enter(tooltip);
+ _mouseHelper.Leave(target);
+
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ _mouseHelper.Enter(other);
+ _mouseHelper.Leave(tooltip);
+
+ Assert.False(ToolTip.GetIsOpen(target));
+ }
+ }
}
internal class ToolTipViewModel