Browse Source

Update popups and flyouts to properly support OverlayDismissEventPassThrough (#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.
release/11.1.2
Bill Henning 2 years ago
committed by Steven Kirk
parent
commit
0d2dad71f1
  1. 18
      src/Avalonia.Controls/Button.cs
  2. 18
      src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
  3. 20
      src/Avalonia.Controls/ComboBox.cs
  4. 25
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  5. 8
      src/Avalonia.Controls/Primitives/Popup.cs
  6. 32
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  7. 2
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  8. 84
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

18
src/Avalonia.Controls/Button.cs

@ -394,13 +394,23 @@ namespace Avalonia.Controls
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();
}
}
}
}

18
src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs

@ -629,10 +629,22 @@ namespace Avalonia.Controls
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)

20
src/Avalonia.Controls/ComboBox.cs

@ -298,7 +298,18 @@ namespace Avalonia.Controls
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/>
@ -314,7 +325,7 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
else
else if (PseudoClasses.Contains(pcPressed))
{
SetCurrentValue(IsDropDownOpenProperty, !IsDropDownOpen);
e.Handled = true;
@ -375,11 +386,6 @@ namespace Avalonia.Controls
{
_subscriptionsOnOpen.Clear();
if (CanFocus(this))
{
Focus();
}
DropDownClosed?.Invoke(this, EventArgs.Empty);
}

25
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@ -41,6 +41,12 @@ namespace Avalonia.Controls.Primitives
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>
@ -115,6 +121,22 @@ namespace Avalonia.Controls.Primitives
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.
@ -247,6 +269,7 @@ namespace Avalonia.Controls.Primitives
Popup.Child = CreatePresenter();
}
Popup.OverlayDismissEventPassThrough = OverlayDismissEventPassThrough;
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
if (CancelOpening())
@ -365,8 +388,6 @@ namespace Avalonia.Controls.Primitives
{
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = true,
//Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
OverlayDismissEventPassThrough = false
};
popup.Opened += OnPopupOpened;

8
src/Avalonia.Controls/Primitives/Popup.cs

@ -745,12 +745,16 @@ namespace Avalonia.Controls.Primitives
{
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();
}
}
}

32
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -226,6 +226,7 @@ namespace Avalonia.Controls
if (_secondaryButton != null)
{
_secondaryButton.Click -= SecondaryButton_Click;
_secondaryButton.RemoveHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed);
}
}
@ -248,6 +249,7 @@ namespace Avalonia.Controls
if (_secondaryButton != null)
{
_secondaryButton.Click += SecondaryButton_Click;
_secondaryButton.AddHandler(PointerPressedEvent, SecondaryButton_PreviewPointerPressed, RoutingStrategies.Tunnel);
}
RegisterFlyoutEvents(Flyout);
@ -422,7 +424,14 @@ namespace Avalonia.Controls
// 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 @@ namespace Avalonia.Controls
}
/// <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)
{
@ -453,7 +462,7 @@ namespace Avalonia.Controls
}
/// <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)
{
@ -461,6 +470,23 @@ namespace Avalonia.Controls
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.

2
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -53,7 +53,7 @@ namespace Avalonia.Controls.UnitTests
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));

84
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@ -221,7 +221,91 @@ namespace Avalonia.Controls.UnitTests
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()
{

Loading…
Cancel
Save