diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index 821aef775889..dc836a08716a 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -394,13 +394,23 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { - IsPressed = true; - e.Handled = true; - - if (ClickMode == ClickMode.Press) + if (_isFlyoutOpen && IsEffectivelyEnabled) { + // When a flyout is open with OverlayDismissEventPassThrough enabled and the button is pressed, + // close the flyout, but do not transition to a pressed state + e.Handled = true; OnClick(); } + else + { + IsPressed = true; + e.Handled = true; + + if (ClickMode == ClickMode.Press) + { + OnClick(); + } + } } } diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index b570b2c4ff00..ec54533f4e14 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -629,10 +629,22 @@ private void TextBox_TextChanged() private void DropDownButton_PointerPressed(object? sender, PointerPressedEventArgs e) { - _ignoreButtonClick = _isPopupClosing; + if (_isFlyoutOpen && (_dropDownButton?.IsEffectivelyEnabled == true) && e.GetCurrentPoint(_dropDownButton).Properties.IsLeftButtonPressed) + { + // When a flyout is open with OverlayDismissEventPassThrough enabled and the drop-down button + // is pressed, close the flyout + _ignoreButtonClick = true; - _isPressed = true; - UpdatePseudoClasses(); + e.Handled = true; + TogglePopUp(); + } + else + { + _ignoreButtonClick = _isPopupClosing; + + _isPressed = true; + UpdatePseudoClasses(); + } } private void DropDownButton_PointerReleased(object? sender, PointerReleasedEventArgs e) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index f1cc84f7a4b1..dbdbf4b536e1 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -298,7 +298,18 @@ protected override void OnPointerPressed(PointerPressedEventArgs e) return; } } - PseudoClasses.Set(pcPressed, true); + + if (IsDropDownOpen) + { + // When a drop-down is open with OverlayDismissEventPassThrough enabled and the control + // is pressed, close the drop-down + SetCurrentValue(IsDropDownOpenProperty, false); + e.Handled = true; + } + else + { + PseudoClasses.Set(pcPressed, true); + } } /// @@ -314,7 +325,7 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) e.Handled = true; } } - else + else if (PseudoClasses.Contains(pcPressed)) { SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen); e.Handled = true; @@ -375,11 +386,6 @@ private void PopupClosed(object? sender, EventArgs e) { _subscriptionsOnOpen.Clear(); - if (CanFocus(this)) - { - Focus(); - } - DropDownClosed?.Invoke(this, EventArgs.Empty); } diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 99c9f065ad3a..b91543556a52 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -41,6 +41,12 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider public static readonly StyledProperty ShowModeProperty = AvaloniaProperty.Register(nameof(ShowMode)); + /// + /// Defines the property + /// + public static readonly StyledProperty OverlayDismissEventPassThroughProperty = + Popup.OverlayDismissEventPassThroughProperty.AddOwner(); + /// /// Defines the property /// @@ -115,6 +121,22 @@ public FlyoutShowMode ShowMode set => SetValue(ShowModeProperty, value); } + /// + /// Gets or sets a value indicating whether the event that closes the flyout is passed + /// through to the parent window. + /// + /// + /// Clicks outside 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 an element that should receive pointer input events even when underneath /// the flyout's overlay. @@ -247,6 +269,7 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = Popup.Child = CreatePresenter(); } + Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough; Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement; if (CancelOpening()) @@ -365,8 +388,6 @@ private Popup CreatePopup() { WindowManagerAddShadowHint = false, IsLightDismissEnabled = true, - //Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss. - OverlayDismissEventPassThrough = false }; popup.Opened += OnPopupOpened; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index a2bf07db0e69..7285281d89cd 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -745,12 +745,16 @@ private void PointerPressedDismissOverlay(object? sender, PointerPressedEventArg { if (IsLightDismissEnabled && e.Source is Visual v && !IsChildOrThis(v)) { - CloseCore(); - if (OverlayDismissEventPassThrough) { PassThroughEvent(e); } + + // Ensure the popup is closed if it was not closed by a pass-through event handler + if (IsOpen) + { + CloseCore(); + } } } diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index dc8dc56fcf40..c51c77bee2a1 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -226,6 +226,7 @@ private void UnregisterEvents() if (_secondaryButton != null) { _secondaryButton.Click -= SecondaryButton_Click; + _secondaryButton.RemoveHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed); } } @@ -248,6 +249,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) if (_secondaryButton != null) { _secondaryButton.Click += SecondaryButton_Click; + _secondaryButton.AddHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed, RoutingStrategies.Tunnel); } RegisterFlyoutEvents(Flyout); @@ -422,7 +424,14 @@ protected virtual void OnClickSecondary(RoutedEventArgs? e) // Note: It is not currently required to check enabled status; however, this is a failsafe if (IsEffectivelyEnabled) { - OpenFlyout(); + if (_isFlyoutOpen) + { + CloseFlyout(); + } + else + { + OpenFlyout(); + } } } @@ -443,7 +452,7 @@ protected virtual void OnFlyoutClosed() } /// - /// Event handler for when the internal primary button part is pressed. + /// Event handler for when the internal primary button part is clicked. /// private void PrimaryButton_Click(object? sender, RoutedEventArgs e) { @@ -453,7 +462,7 @@ private void PrimaryButton_Click(object? sender, RoutedEventArgs e) } /// - /// Event handler for when the internal secondary button part is pressed. + /// Event handler for when the internal secondary button part is clicked. /// private void SecondaryButton_Click(object? sender, RoutedEventArgs e) { @@ -461,6 +470,23 @@ private void SecondaryButton_Click(object? sender, RoutedEventArgs e) e.Handled = true; OnClickSecondary(e); } + + /// + /// Event handler for when the internal secondary button part is pressed. + /// + private void SecondaryButton_PreviewPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (_isFlyoutOpen && _secondaryButton?.IsEffectivelyEnabled == true) + { + if (e.GetCurrentPoint(_secondaryButton).Properties.IsLeftButtonPressed) + { + // When a flyout is open with OverlayDismissEventPassThrough enabled and the secondary button + // is pressed, close the flyout + e.Handled = true; + OnClickSecondary(e); + } + } + } /// /// Called when the property changes. diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 162e3a6f8e75..65243fa445e9 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -53,7 +53,7 @@ public void Clicking_On_Control_PseudoClass() Assert.True(target.Classes.Contains(ComboBox.pcDropdownOpen)); _helper.Down(target); - Assert.True(target.Classes.Contains(ComboBox.pcPressed)); + Assert.True(!target.Classes.Contains(ComboBox.pcPressed)); _helper.Up(target); Assert.True(!target.Classes.Contains(ComboBox.pcPressed)); diff --git a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs index e3f598327410..dad8c3b78e7b 100644 --- a/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs +++ b/tests/Avalonia.Controls.UnitTests/FlyoutTests.cs @@ -221,7 +221,91 @@ public void Light_Dismiss_Closes_Flyout() Assert.False(f.IsOpen); } } + + [Fact] + public void Light_Dismiss_No_Event_Pass_Through_To_Button() + { + using (CreateServicesWithFocus()) + { + var window = PreparedWindow(); + window.Width = 100; + window.Height = 100; + + bool buttonClicked = false; + var button = new Button() + { + ClickMode = ClickMode.Press + }; + button.Click += (s, e) => + { + buttonClicked = true; + }; + window.Content = button; + + window.Show(); + + var f = new Flyout(); + f.OverlayDismissEventPassThrough = false; // Focus of test + f.Content = new Border { Width = 10, Height = 10 }; + f.ShowAt(window); + + var hitTester = new Mock(); + window.HitTesterOverride = hitTester.Object; + hitTester.Setup(x => + x.HitTestFirst(new Point(90, 90), window, It.IsAny>())) + .Returns(button); + + var e = CreatePointerPressedEventArgs(window, new Point(90, 90)); + var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window); + overlay.RaiseEvent(e); + + Assert.False(f.IsOpen); + Assert.False(buttonClicked); // Button is NOT clicked + } + } + [Fact] + public void Light_Dismiss_Event_Pass_Through_To_Button() + { + using (CreateServicesWithFocus()) + { + var window = PreparedWindow(); + window.Width = 100; + window.Height = 100; + + bool buttonClicked = false; + var button = new Button() + { + ClickMode = ClickMode.Press + }; + button.Click += (s, e) => + { + buttonClicked = true; + }; + window.Content = button; + + window.Show(); + + var f = new Flyout(); + f.OverlayDismissEventPassThrough = true; // Focus of test + f.Content = new Border { Width = 10, Height = 10 }; + f.ShowAt(window); + + var hitTester = new Mock(); + window.HitTesterOverride = hitTester.Object; + hitTester.Setup(x => + x.HitTestFirst(new Point(90, 90), window, It.IsAny>())) + .Returns(button); + + var e = CreatePointerPressedEventArgs(window, new Point(90, 90)); + var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window); + overlay.RaiseEvent(e); + + Assert.False(f.IsOpen); + Assert.True(buttonClicked); // Button is clicked + } + } + [Fact] public void Flyout_Has_Uncancellable_Close_Before_Showing_On_A_Different_Target() {