diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs
index 31101dc0f1..c164f282e8 100644
--- a/src/Avalonia.Controls/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox.cs
@@ -1647,7 +1647,7 @@ namespace Avalonia.Controls
///
/// The source object.
/// The event data.
- private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
+ private void DropDownPopup_Closed(object sender, EventArgs e)
{
// Force the drop down dependency property to be false.
if (IsDropDownOpen)
@@ -1655,11 +1655,6 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
}
- if (e.CloseEvent is PointerEventArgs pointerEvent)
- {
- pointerEvent.Handled = true;
- }
-
// Fire the DropDownClosed event
if (_popupHasOpened)
{
diff --git a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
index b987f065be..046b55d49a 100644
--- a/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
@@ -889,17 +889,12 @@ namespace Avalonia.Controls
_ignoreButtonClick = false;
}
}
- private void PopUp_Closed(object sender, PopupClosedEventArgs e)
+ private void PopUp_Closed(object sender, EventArgs e)
{
IsDropDownOpen = false;
if(!_isPopupClosing)
{
- if (e.CloseEvent is PointerEventArgs pointerEvent)
- {
- pointerEvent.Handled = true;
- }
-
_isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
}
diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs
index 75a0e41e7b..27313b0b4c 100644
--- a/src/Avalonia.Controls/ComboBox.cs
+++ b/src/Avalonia.Controls/ComboBox.cs
@@ -290,24 +290,6 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get("PART_Popup");
_popup.Opened += PopupOpened;
- _popup.Closed += PopupClosed;
- }
-
- ///
- /// Called when the ComboBox popup is closed, with the
- /// that caused the popup to close.
- ///
- /// The event args.
- ///
- /// This method can be overridden to control whether the event that caused the popup to close
- /// is swallowed or passed through.
- ///
- protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
- {
- if (e.CloseEvent is PointerEventArgs pointerEvent)
- {
- pointerEvent.Handled = true;
- }
}
internal void ItemFocused(ComboBoxItem dropDownItem)
@@ -318,13 +300,11 @@ namespace Avalonia.Controls
}
}
- private void PopupClosed(object sender, PopupClosedEventArgs e)
+ private void PopupClosed(object sender, EventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
- PopupClosedOverride(e);
-
if (CanFocus(this))
{
Focus();
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 7720011d5e..b4e4dd5071 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -266,6 +266,7 @@ namespace Avalonia.Controls
PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
IsLightDismissEnabled = true,
+ OverlayDismissEventPassThrough = true,
};
_popup.Opened += PopupOpened;
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 7272a565d9..c97d90baed 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/src/Avalonia.Controls/Primitives/Popup.cs
@@ -1,12 +1,10 @@
using System;
-using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
-using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Platform;
@@ -86,6 +84,9 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register(nameof(ObeyScreenEdges), true);
#pragma warning restore 618
+ public static readonly StyledProperty OverlayDismissEventPassThroughProperty =
+ AvaloniaProperty.Register(nameof(OverlayDismissEventPassThrough));
+
///
/// Defines the property.
///
@@ -138,7 +139,7 @@ namespace Avalonia.Controls.Primitives
///
/// Raised when the popup closes.
///
- public event EventHandler? Closed;
+ public event EventHandler? Closed;
///
/// Raised when the popup opens.
@@ -179,6 +180,9 @@ namespace Avalonia.Controls.Primitives
///
/// Gets or sets a value that determines how the can be dismissed.
///
+ ///
+ /// Light dismiss is when the user taps on any area other than the popup.
+ ///
public bool IsLightDismissEnabled
{
get => GetValue(IsLightDismissEnabledProperty);
@@ -266,6 +270,22 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ObeyScreenEdgesProperty, value);
}
+ ///
+ /// Gets or sets a value indicating whether the event that closes the popup is passed
+ /// through to the parent window.
+ ///
+ ///
+ /// When is set to true, clicks outside the the popup
+ /// cause the popup to close. When is set to
+ /// false, these clicks will be handled by the popup and not be registered by the parent
+ /// window. When set to true, the events will be passed through to the parent window.
+ ///
+ public bool OverlayDismissEventPassThrough
+ {
+ get => GetValue(OverlayDismissEventPassThroughProperty);
+ set => SetValue(OverlayDismissEventPassThroughProperty, value);
+ }
+
///
/// Gets or sets the Horizontal offset of the popup in relation to the .
///
@@ -384,7 +404,7 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
- DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed,
+ DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
}
@@ -436,7 +456,7 @@ namespace Avalonia.Controls.Primitives
///
/// Closes the popup.
///
- public void Close() => CloseCore(null);
+ public void Close() => CloseCore();
///
/// Measures the control.
@@ -506,7 +526,7 @@ namespace Avalonia.Controls.Primitives
}
}
- private void CloseCore(EventArgs? closeEvent)
+ private void CloseCore()
{
if (_openState is null)
{
@@ -526,7 +546,7 @@ namespace Avalonia.Controls.Primitives
IsOpen = false;
}
- Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
+ Closed?.Invoke(this, EventArgs.Empty);
}
private void ListenForNonClientClick(RawInputEventArgs e)
@@ -535,7 +555,7 @@ namespace Avalonia.Controls.Primitives
if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
- CloseCore(e);
+ CloseCore();
}
}
@@ -543,7 +563,28 @@ namespace Avalonia.Controls.Primitives
{
if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v))
{
- CloseCore(e);
+ CloseCore();
+
+ if (OverlayDismissEventPassThrough)
+ {
+ PassThroughEvent(e);
+ }
+ }
+ }
+
+ private void PassThroughEvent(PointerPressedEventArgs e)
+ {
+ if (e.Source is LightDismissOverlayLayer layer &&
+ layer.GetVisualRoot() is IInputElement root)
+ {
+ var p = e.GetCurrentPoint(root);
+ var hit = root.InputHitTest(p.Position, x => x != layer);
+
+ if (hit != null)
+ {
+ hit.RaiseEvent(e);
+ e.Handled = true;
+ }
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
deleted file mode 100644
index db554b3c82..0000000000
--- a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-using Avalonia.Interactivity;
-
-#nullable enable
-
-namespace Avalonia.Controls.Primitives
-{
- ///
- /// Holds data for the event.
- ///
- public class PopupClosedEventArgs : EventArgs
- {
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- public PopupClosedEventArgs(EventArgs? closeEvent)
- {
- CloseEvent = closeEvent;
- }
-
- ///
- /// Gets the event that closed the popup, if any.
- ///
- ///
- /// If is true, then this property will hold details of the
- /// interaction that caused the popup to close if the close was caused by e.g. a pointer press
- /// outside the popup. It can be used to mark the event as handled if the event should not
- /// be propagated.
- ///
- public EventArgs? CloseEvent { get; }
- }
-}
diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs
index 4babe711f2..cbe36583e6 100644
--- a/src/Avalonia.Input/InputExtensions.cs
+++ b/src/Avalonia.Input/InputExtensions.cs
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
+#nullable enable
+
namespace Avalonia.Input
{
///
@@ -22,7 +24,7 @@ namespace Avalonia.Input
///
public static IEnumerable GetInputElementsAt(this IInputElement element, Point p)
{
- Contract.Requires(element != null);
+ element = element ?? throw new ArgumentNullException(nameof(element));
return element.GetVisualsAt(p, s_hitTestDelegate).Cast();
}
@@ -33,13 +35,34 @@ namespace Avalonia.Input
/// The element to test.
/// The point on .
/// The topmost at the specified position.
- public static IInputElement InputHitTest(this IInputElement element, Point p)
+ public static IInputElement? InputHitTest(this IInputElement element, Point p)
{
- Contract.Requires(element != null);
+ element = element ?? throw new ArgumentNullException(nameof(element));
return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement;
}
+ ///
+ /// Returns the topmost active input element at a point on an .
+ ///
+ /// The element to test.
+ /// The point on .
+ ///
+ /// A filter predicate. If the predicate returns false then the visual and all its
+ /// children will be excluded from the results.
+ ///
+ /// The topmost at the specified position.
+ public static IInputElement? InputHitTest(
+ this IInputElement element,
+ Point p,
+ Func filter)
+ {
+ element = element ?? throw new ArgumentNullException(nameof(element));
+ filter = filter ?? throw new ArgumentNullException(nameof(filter));
+
+ return element.GetVisualAt(p, x => s_hitTestDelegate(x) && filter(x)) as IInputElement;
+ }
+
private static bool IsHitTestVisible(IVisual visual)
{
var element = visual as IInputElement;
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index 1d5b39cc14..422ade2f90 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -349,53 +349,42 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
- public void LightDismiss_Should_Not_Handle_Closing_Click()
+ public void OverlayDismissEventPassThrough_Should_Pass_Event_To_Window_Contents()
{
using (CreateServices())
{
var window = PreparedWindow();
+ var rendererMock = Mock.Get(window.Renderer);
var target = new Popup()
{
PlacementTarget = window ,
IsLightDismissEnabled = true,
+ OverlayDismissEventPassThrough = true,
};
- target.Open();
-
- var e = CreatePointerPressedEventArgs(window);
- window.RaiseEvent(e);
+ var raised = 0;
+ var border = new Border();
+ window.Content = border;
- Assert.False(e.Handled);
- }
- }
+ rendererMock.Setup(x =>
+ x.HitTestFirst(new Point(10, 15), window, It.IsAny>()))
+ .Returns(border);
- [Fact]
- public void Should_Pass_Closing_Click_To_Closed_Event()
- {
- using (CreateServices())
- {
- var window = PreparedWindow();
- var target = new Popup()
+ border.PointerPressed += (s, e) =>
{
- PlacementTarget = window,
- IsLightDismissEnabled = true,
+ Assert.Same(border, e.Source);
+ ++raised;
};
target.Open();
+ Assert.True(target.IsOpen);
- var press = CreatePointerPressedEventArgs(window);
- var raised = 0;
-
- target.Closed += (s, e) =>
- {
- Assert.Same(press, e.CloseEvent);
- ++raised;
- };
-
- var lightDismissLayer = window.FindDescendantOfType().LightDismissOverlayLayer;
- lightDismissLayer.RaiseEvent(press);
+ var e = CreatePointerPressedEventArgs(window, new Point(10, 15));
+ var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
+ overlay.RaiseEvent(e);
Assert.Equal(1, raised);
+ Assert.False(target.IsOpen);
}
}
@@ -411,14 +400,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
})));
}
- private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source)
+ private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p)
{
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
return new PointerPressedEventArgs(
source,
pointer,
source,
- default,
+ p,
0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
KeyModifiers.None);
diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
index 48a333dc54..9b77fbb009 100644
--- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
+++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
@@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Moq;
using Avalonia.Platform;
+using Avalonia.Rendering;
namespace Avalonia.UnitTests
{
@@ -39,6 +40,9 @@ namespace Avalonia.UnitTests
return CreatePopupMock(windowImpl.Object).Object;
});
+ windowImpl.Setup(x => x.CreateRenderer(It.IsAny()))
+ .Returns(Mock.Of());
+
windowImpl.Setup(x => x.Dispose()).Callback(() =>
{
windowImpl.Object.Closed?.Invoke();