Skip to content

Commit

Permalink
Update popups and flyouts to properly support OverlayDismissEventPass…
Browse files Browse the repository at this point in the history
…Through (#15517)

* Updated Popup to raise the pass-through overlay dismiss event prior to possibly closing the popup when a pointer is pressed.  Added the PopupFlyoutBase.OverlayDismissEventPassThrough property and updated logic in Button.

* Updated SplitButton logic to handle OverlayDismissEventPassThrough scenarios.

* Updated CalendarDatePicker logic to handle OverlayDismissEventPassThrough scenarios.

* Updated ComboBox logic to handle OverlayDismissEventPassThrough scenarios.

* Removed unncessary ComboBox.PopupClosed logic that focused the control.  This was problematic when the popup was open with OverlayDismissEventPassThrough and clicking onto another control.  Focus would not move to the clicked control.

* Fixed the Clicking_On_Control_PseudoClass unit test to properly recognize pseudo-class behavior change.

* Added a couple unit tests to FlyoutTests.
  • Loading branch information
billhenn authored and grokys committed Aug 2, 2024
1 parent fcd4c22 commit 0d2dad7
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 22 deletions.
18 changes: 14 additions & 4 deletions src/Avalonia.Controls/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
}

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 @@ -247,6 +269,7 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer =
Popup.Child = CreatePresenter();
}

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

if (CancelOpening())
Expand Down Expand Up @@ -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;
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 @@ -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();
}
}
}

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 @@ -226,6 +226,7 @@ private void UnregisterEvents()
if (_secondaryButton != null)
{
_secondaryButton.Click -= SecondaryButton_Click;
_secondaryButton.RemoveHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed);
}
}

Expand All @@ -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);
Expand Down Expand Up @@ -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();
}
}
}

Expand All @@ -443,7 +452,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 @@ -453,14 +462,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

0 comments on commit 0d2dad7

Please sign in to comment.