Browse Source

Don't hide tooltip when pointer is over it. (#13565)

* Add failing test for #8638.

* Don't hide tooltip when pointer is over tooltip.

Fixes #8638

* Close the tooltip when pointer exits.

If the pointer has been moved from the control to the tooltip, then out of the tooltip to another control, ensure that the tooltip is closed.

* AdornedControl can be a standard CLR property.
Steven Kirk 1 year ago
parent
commit
59d44e97bf

+ 11 - 7
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;
+
         /// <summary>
         /// Gets the value of the ToolTip.Tip attached property.
         /// </summary>
@@ -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);
             }
         }
 

+ 32 - 1
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;
+                }
             }
         }
 

+ 107 - 0
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<ToolTip>(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<ToolTip>(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<ToolTip>(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