diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index bf177d64cd..9bc7ba9e2f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1630,7 +1630,7 @@ namespace Avalonia.Controls /// /// The source object. /// The event data. - private void DropDownPopup_Closed(object sender, EventArgs e) + private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e) { // Force the drop down dependency property to be false. if (IsDropDownOpen) @@ -1638,6 +1638,11 @@ 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/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 07e42c64e4..b4e4ad1452 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -895,12 +895,17 @@ namespace Avalonia.Controls _ignoreButtonClick = false; } } - private void PopUp_Closed(object sender, EventArgs e) + private void PopUp_Closed(object sender, PopupClosedEventArgs 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 4b7d931d80..1daa6a5630 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -242,11 +242,16 @@ namespace Avalonia.Controls } } - private void PopupClosed(object sender, EventArgs e) + private void PopupClosed(object sender, PopupClosedEventArgs e) { _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + if (CanFocus(this)) { Focus(); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 932800868c..66f2153b6c 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when the popup closes. /// - public event EventHandler? Closed; + public event EventHandler? Closed; /// /// Raised when the popup opens. @@ -270,7 +270,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)); } @@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives /// /// Closes the popup. /// - public void Close() - { - if (_openState is null) - { - using (BeginIgnoringIsOpen()) - { - IsOpen = false; - } - - return; - } - - _openState.Dispose(); - _openState = null; - - using (BeginIgnoringIsOpen()) - { - IsOpen = false; - } - - Closed?.Invoke(this, EventArgs.Empty); - } + public void Close() => CloseCore(null); /// /// Measures the control. @@ -389,21 +368,44 @@ namespace Avalonia.Controls.Primitives } } + private void CloseCore(EventArgs? closeEvent) + { + if (_openState is null) + { + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + + return; + } + + _openState.Dispose(); + _openState = null; + + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + + Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent)); + } + private void ListenForNonClientClick(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { - Close(); + CloseCore(e); } } private void PointerPressedOutside(object sender, PointerPressedEventArgs e) { - if (!StaysOpen && !IsChildOrThis((IVisual)e.Source)) + if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v)) { - Close(); + CloseCore(e); } } diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs new file mode 100644 index 0000000000..c51543438c --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs @@ -0,0 +1,33 @@ +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 false, 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/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 4321d566df..a479317f3d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -358,21 +358,42 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Open(); - var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); - var e = new PointerPressedEventArgs( - window, - pointer, - window, - default, - 0, - new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), - KeyModifiers.None); + var e = CreatePointerPressedEventArgs(window); window.RaiseEvent(e); Assert.False(e.Handled); } } + [Fact] + public void Should_Pass_Closing_Click_To_Closed_Event() + { + using (CreateServices()) + { + var window = PreparedWindow(); + var target = new Popup() + { + PlacementTarget = window, + StaysOpen = false, + }; + + target.Open(); + + var press = CreatePointerPressedEventArgs(window); + var raised = 0; + + target.Closed += (s, e) => + { + Assert.Same(press, e.CloseEvent); + ++raised; + }; + + window.RaiseEvent(press); + + Assert.Equal(1, raised); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: @@ -385,6 +406,19 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } + private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source) + { + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + return new PointerPressedEventArgs( + source, + pointer, + source, + default, + 0, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), + KeyModifiers.None); + } + private Window PreparedWindow(object content = null) { var w = new Window { Content = content };