Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update popups and flyouts to properly support OverlayDismissEventPassThrough #15517

Merged
18 changes: 14 additions & 4 deletions src/Avalonia.Controls/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,13 +390,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();
}
}
}
}

Expand Down
18 changes: 15 additions & 3 deletions src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 13 additions & 7 deletions src/Avalonia.Controls/ComboBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

/// <inheritdoc/>
Expand All @@ -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;
Expand Down Expand Up @@ -375,11 +386,6 @@ private void PopupClosed(object? sender, EventArgs e)
{
_subscriptionsOnOpen.Clear();

if (CanFocus(this))
{
Focus();
}

DropDownClosed?.Invoke(this, EventArgs.Empty);
}

Expand Down
25 changes: 23 additions & 2 deletions src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<PopupFlyoutBase, FlyoutShowMode>(nameof(ShowMode));

/// <summary>
/// Defines the <see cref="OverlayDismissEventPassThrough"/> property
/// </summary>
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
Popup.OverlayDismissEventPassThroughProperty.AddOwner<PopupFlyoutBase>();

/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
Expand Down Expand Up @@ -115,6 +121,22 @@ public FlyoutShowMode ShowMode
set => SetValue(ShowModeProperty, value);
}

/// <summary>
/// Gets or sets a value indicating whether the event that closes the flyout is passed
/// through to the parent window.
/// </summary>
/// <remarks>
/// Clicks outside the popup cause the popup to close. When
/// <see cref="OverlayDismissEventPassThrough"/> 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.
/// </remarks>
public bool OverlayDismissEventPassThrough
{
get => GetValue(OverlayDismissEventPassThroughProperty);
set => SetValue(OverlayDismissEventPassThroughProperty, value);
}

/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the flyout's overlay.
Expand Down Expand Up @@ -239,6 +261,7 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer =
Popup.Child = CreatePresenter();
}

Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough;
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;

if (CancelOpening())
Expand Down Expand Up @@ -357,8 +380,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;
Expand Down
8 changes: 6 additions & 2 deletions src/Avalonia.Controls/Primitives/Popup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -766,12 +766,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();
}
}
}

Expand Down
32 changes: 29 additions & 3 deletions src/Avalonia.Controls/SplitButton/SplitButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ private void UnregisterEvents()
if (_secondaryButton != null)
{
_secondaryButton.Click -= SecondaryButton_Click;
_secondaryButton.RemoveHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed);
}
}

Expand All @@ -237,6 +238,7 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
if (_secondaryButton != null)
{
_secondaryButton.Click += SecondaryButton_Click;
_secondaryButton.AddHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed, RoutingStrategies.Tunnel);
}

RegisterFlyoutEvents(Flyout);
Expand Down Expand Up @@ -409,7 +411,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();
}
}
}

Expand All @@ -430,7 +439,7 @@ protected virtual void OnFlyoutClosed()
}

/// <summary>
/// Event handler for when the internal primary button part is pressed.
/// Event handler for when the internal primary button part is clicked.
/// </summary>
private void PrimaryButton_Click(object? sender, RoutedEventArgs e)
{
Expand All @@ -440,14 +449,31 @@ private void PrimaryButton_Click(object? sender, RoutedEventArgs e)
}

/// <summary>
/// Event handler for when the internal secondary button part is pressed.
/// Event handler for when the internal secondary button part is clicked.
/// </summary>
private void SecondaryButton_Click(object? sender, RoutedEventArgs e)
{
// Handle internal button click, so it won't bubble outside.
e.Handled = true;
OnClickSecondary(e);
}

/// <summary>
/// Event handler for when the internal secondary button part is pressed.
/// </summary>
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);
}
}
}

/// <summary>
/// Called when the <see cref="PopupFlyoutBase.Placement"/> property changes.
Expand Down
2 changes: 1 addition & 1 deletion tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
84 changes: 84 additions & 0 deletions tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IHitTester>();
window.HitTesterOverride = hitTester.Object;
hitTester.Setup(x =>
x.HitTestFirst(new Point(90, 90), window, It.IsAny<Func<Visual, bool>>()))
.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<IHitTester>();
window.HitTesterOverride = hitTester.Object;
hitTester.Setup(x =>
x.HitTestFirst(new Point(90, 90), window, It.IsAny<Func<Visual, bool>>()))
.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()
{
Expand Down
Loading