diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs
index 7d6a2c2c3e..f306ce84af 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml.cs
+++ b/samples/IntegrationTestApp/MainWindow.axaml.cs
@@ -89,6 +89,7 @@ namespace IntegrationTestApp
new("ListBox", () => new ListBoxPage()),
new("Menu", () => new MenuPage()),
new("Pointer", () => new PointerPage()),
+ new("Popups", () => new PopupsPage()),
new("RadioButton", () => new RadioButtonPage()),
new("Screens", () => new ScreensPage()),
new("ScrollBar", () => new ScrollBarPage()),
diff --git a/samples/IntegrationTestApp/Pages/PopupsPage.axaml b/samples/IntegrationTestApp/Pages/PopupsPage.axaml
new file mode 100644
index 0000000000..58ee76f2a0
--- /dev/null
+++ b/samples/IntegrationTestApp/Pages/PopupsPage.axaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/IntegrationTestApp/Pages/PopupsPage.axaml.cs b/samples/IntegrationTestApp/Pages/PopupsPage.axaml.cs
new file mode 100644
index 0000000000..feca539d90
--- /dev/null
+++ b/samples/IntegrationTestApp/Pages/PopupsPage.axaml.cs
@@ -0,0 +1,48 @@
+using Avalonia;
+using Avalonia.Automation;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+
+namespace IntegrationTestApp.Pages;
+
+public partial class PopupsPage : UserControl
+{
+ public PopupsPage()
+ {
+ InitializeComponent();
+ }
+
+ private void ButtonLightDismiss_OnClick(object sender, RoutedEventArgs e)
+ {
+ LightDismissPopup.Open();
+ }
+
+ private void ButtonPopupStaysOpen_OnClick(object sender, RoutedEventArgs e)
+ {
+ StaysOpenPopup.Open();
+ }
+ private void StaysOpenPopupCloseButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ StaysOpenPopup.Close();
+ }
+
+ private void ButtonTopMostPopupStaysOpen(object sender, RoutedEventArgs e)
+ {
+ TopMostPopup.Open();
+ }
+ private void TopMostPopupCloseButton_OnClick(object? sender, RoutedEventArgs e)
+ {
+ TopMostPopup.Close();
+ }
+
+ private void OpenRegularNewWindow_Click(object? sender, RoutedEventArgs e)
+ {
+ var newWindow = new ShowWindowTest();
+ newWindow.Show((Window)TopLevel.GetTopLevel(this)!);
+ }
+}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index 07ac06f15d..d1fd1317f8 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs
@@ -215,6 +215,10 @@ namespace Avalonia.Controls.Primitives
{
PlatformImpl?.SetWindowManagerAddShadowHint(change.GetNewValue());
}
+ else if (change.Property == TopmostProperty)
+ {
+ PlatformImpl?.SetTopmost(change.GetNewValue());
+ }
}
}
}
diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs
index fd7b0982c4..ee2feedffc 100644
--- a/src/Windows/Avalonia.Win32/PopupImpl.cs
+++ b/src/Windows/Avalonia.Win32/PopupImpl.cs
@@ -1,7 +1,10 @@
using System;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Input;
using Avalonia.Platform;
+using Avalonia.Threading;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
@@ -53,8 +56,7 @@ namespace Avalonia.Win32
UnmanagedMethods.WindowStyles.WS_CLIPCHILDREN;
UnmanagedMethods.WindowStyles exStyle =
- UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW |
- UnmanagedMethods.WindowStyles.WS_EX_TOPMOST;
+ UnmanagedMethods.WindowStyles.WS_EX_TOOLWINDOW;
var result = UnmanagedMethods.CreateWindowEx(
(int)exStyle,
diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs
index 6fd9505583..e2670d25e4 100644
--- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs
+++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs
@@ -1,11 +1,13 @@
-using OpenQA.Selenium;
+using System;
+using System.Threading;
+using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
using Xunit;
namespace Avalonia.IntegrationTests.Appium
{
[Collection("Default")]
- public abstract class MenuTests : TestBase
+ public abstract class MenuTests : TestBase, IDisposable
{
public MenuTests(DefaultAppFixture fixture)
: base(fixture, "Menu")
@@ -17,11 +19,18 @@ namespace Avalonia.IntegrationTests.Appium
Assert.Equal("None", clickedMenuItem.Text);
}
+ public void Dispose()
+ {
+ // Click the reset button so that any menu still open gets closed
+ var reset = Session.FindElementByAccessibilityId("MenuClickedMenuItemReset");
+ reset?.Click();
+ }
+
[Fact]
public void Click_Child()
{
var rootMenuItem = Session.FindElementByAccessibilityId("RootMenuItem");
-
+
rootMenuItem.SendClick();
var childMenuItem = Session.FindElementByAccessibilityId("Child1MenuItem");
@@ -35,7 +44,7 @@ namespace Avalonia.IntegrationTests.Appium
public void Click_Grandchild()
{
var rootMenuItem = Session.FindElementByAccessibilityId("RootMenuItem");
-
+
rootMenuItem.SendClick();
var childMenuItem = Session.FindElementByAccessibilityId("Child2MenuItem");
@@ -53,10 +62,12 @@ namespace Avalonia.IntegrationTests.Appium
{
MovePointerOutOfTheWay();
- new Actions(Session)
- .KeyDown(Keys.Alt).KeyUp(Keys.Alt)
- .SendKeys(Keys.Down + Keys.Enter)
- .Perform();
+ var window = Session.FindElementByAccessibilityId("MainWindow");
+ window.Click();
+
+ window.SendKeys(Keys.LeftAlt);
+ Thread.Sleep(150);
+ window.SendKeys(Keys.Down + Keys.Enter);
var clickedMenuItem = Session.FindElementByAccessibilityId("ClickedMenuItem");
Assert.Equal("_Child 1", clickedMenuItem.Text);
@@ -67,10 +78,12 @@ namespace Avalonia.IntegrationTests.Appium
{
MovePointerOutOfTheWay();
- new Actions(Session)
- .KeyDown(Keys.Alt).KeyUp(Keys.Alt)
- .SendKeys(Keys.Down + Keys.Down + Keys.Right + Keys.Enter)
- .Perform();
+ var window = Session.FindElementByAccessibilityId("MainWindow");
+ window.Click();
+
+ window.SendKeys(Keys.LeftAlt);
+ Thread.Sleep(150);
+ window.SendKeys(Keys.Down + Keys.Down + Keys.Right + Keys.Enter);
var clickedMenuItem = Session.FindElementByAccessibilityId("ClickedMenuItem");
Assert.Equal("_Grandchild", clickedMenuItem.Text);
@@ -81,10 +94,14 @@ namespace Avalonia.IntegrationTests.Appium
{
MovePointerOutOfTheWay();
- new Actions(Session)
- .KeyDown(Keys.Alt).KeyUp(Keys.Alt)
- .SendKeys("rc")
- .Perform();
+ var window = Session.FindElementByAccessibilityId("MainWindow");
+ window.Click();
+
+ window.SendKeys(Keys.LeftAlt);
+ Thread.Sleep(150);
+ window.SendKeys("r");
+ Thread.Sleep(75);
+ window.SendKeys("c");
var clickedMenuItem = Session.FindElementByAccessibilityId("ClickedMenuItem");
Assert.Equal("_Child 1", clickedMenuItem.Text);
@@ -95,10 +112,16 @@ namespace Avalonia.IntegrationTests.Appium
{
MovePointerOutOfTheWay();
- new Actions(Session)
- .KeyDown(Keys.Alt).KeyUp(Keys.Alt)
- .SendKeys("rhg")
- .Perform();
+ var window = Session.FindElementByAccessibilityId("MainWindow");
+ window.Click();
+
+ window.SendKeys(Keys.LeftAlt);
+ Thread.Sleep(150);
+ window.SendKeys("r");
+ Thread.Sleep(75);
+ window.SendKeys("h");
+ Thread.Sleep(75);
+ window.SendKeys("g");
var clickedMenuItem = Session.FindElementByAccessibilityId("ClickedMenuItem");
Assert.Equal("_Grandchild", clickedMenuItem.Text);
diff --git a/tests/Avalonia.IntegrationTests.Appium/PopupsTests.cs b/tests/Avalonia.IntegrationTests.Appium/PopupsTests.cs
new file mode 100644
index 0000000000..98f6bc804d
--- /dev/null
+++ b/tests/Avalonia.IntegrationTests.Appium/PopupsTests.cs
@@ -0,0 +1,199 @@
+using System.Threading;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Interactions;
+using Xunit;
+
+namespace Avalonia.IntegrationTests.Appium;
+
+[Collection("Default")]
+public abstract class PopupsTests : TestBase
+{
+ private readonly bool _isOverlayPopups;
+
+ protected PopupsTests(bool isOverlayPopups, DefaultAppFixture fixture) : base(fixture, "Popups")
+ {
+ _isOverlayPopups = isOverlayPopups;
+ }
+
+ [PlatformFact(TestPlatforms.Windows)]
+ public void LightDismiss_Popup_Should_Open_And_Close()
+ {
+ // Open popup
+ var button = Session.FindElementByAccessibilityId("ShowLightDismissPopup");
+ button.Click();
+ Thread.Sleep(500);
+
+ // Assert - Popup is visible
+ Assert.NotNull(Session.FindElementByAccessibilityId("LightDismissPopupContent"));
+
+ // Act - Click outside to dismiss
+ var dismissBorder = Session.FindElementByAccessibilityId("DismissButton");
+ dismissBorder.Click();
+
+ Thread.Sleep(500);
+
+ // Assert - Popup is closed
+ Assert.Throws(() =>
+ Session.FindElementByAccessibilityId("LightDismissPopupContent"));
+ }
+
+ [PlatformFact(TestPlatforms.Windows)]
+ public void StaysOpen_Popup_Should_Stay_Open()
+ {
+ // Open popup
+ var button = Session.FindElementByAccessibilityId("ShowStaysOpenPopup");
+ button.Click();
+ Thread.Sleep(500);
+
+ try
+ {
+ // Assert - Popup is visible
+ Assert.NotNull(Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton"));
+
+ // Act - Click outside
+ var dismissBorder = Session.FindElementByAccessibilityId("DismissButton");
+ dismissBorder.Click();
+
+ Thread.Sleep(500);
+
+ // Assert - Popup is still visible
+ Assert.NotNull(Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton"));
+
+ }
+ finally
+ {
+ // Act - Close popup with button
+ Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton").Click();
+
+ Thread.Sleep(400);
+
+ // Assert - Popup is closed
+ Assert.Throws(() =>
+ Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton"));
+ }
+ }
+
+ [PlatformFact(TestPlatforms.Windows)]
+ public void StaysOpen_Popup_TextBox_Should_Be_Editable()
+ {
+ // Open popup
+ var button = Session.FindElementByAccessibilityId("ShowStaysOpenPopup");
+ button.Click();
+ Thread.Sleep(500);
+
+ try
+ {
+ // Find and edit the TextBox
+ var textBox = Session.FindElementByAccessibilityId("StaysOpenTextBox");
+ textBox.Clear();
+ textBox.SendKeys("New text value");
+ Thread.Sleep(500);
+
+ // Verify text was changed
+ Assert.Equal("New text value", textBox.Text);
+ }
+ finally
+ {
+ // Cleanup - close popup
+ Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton").Click();
+ }
+ }
+
+ [PlatformFact(TestPlatforms.Windows)]
+ public void TopMost_Popup_Should_Stay_Above_Other_Windows()
+ {
+ // It's not possible to test overlay topmost with other windows.
+ if (_isOverlayPopups)
+ {
+ return;
+ }
+
+ var staysOpenPopup = Session.FindElementByAccessibilityId("ShowTopMostPopup");
+ var mainWindowHandle = Session.CurrentWindowHandle;
+
+ // Show topmost popup.
+ staysOpenPopup.Click();
+ Assert.NotNull(Session.FindElementByAccessibilityId("TopMostPopupCloseButton"));
+
+ var hasClosedPopup = false;
+
+ try
+ {
+ // Open a child window.
+ using var _ = Session.FindElementByAccessibilityId("OpenNewWindowButton").OpenWindowWithClick();
+ Thread.Sleep(500);
+
+ // Force window to front by maximizing child window.
+ Session.FindElementByAccessibilityId("CurrentWindowState").SendClick();
+ Session.FindElementByAccessibilityId("WindowStateMaximized").SendClick();
+
+ // Switch back to the mainwindow context and verify tooltip is still accessible.
+ Session.SwitchTo().Window(mainWindowHandle);
+ Assert.NotNull(Session.FindElementByAccessibilityId("TopMostPopupCloseButton"));
+
+ // Verify we can still interact with the popup by closing it via button.
+ Session.FindElementByAccessibilityId("TopMostPopupCloseButton").Click();
+
+ // Verify popup closed
+ Assert.Throws(() =>
+ Session.FindElementByAccessibilityId("TopMostPopupCloseButton"));
+ hasClosedPopup = true;
+ }
+ finally
+ {
+ if (!hasClosedPopup)
+ {
+ Session.FindElementByAccessibilityId("TopMostPopupCloseButton").Click();
+ }
+ }
+ }
+
+ [PlatformFact(TestPlatforms.Windows)]
+ public void Non_TopMost_Popup_Does_Not_Stay_Above_Other_Windows()
+ {
+ var topmostButton = Session.FindElementByAccessibilityId("ShowStaysOpenPopup");
+ var mainWindowHandle = Session.CurrentWindowHandle;
+
+ // Show topmost popup.
+ topmostButton.Click();
+ Assert.NotNull(Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton"));
+
+ try
+ {
+ // Open a child window.
+ var newWindowButton = Session.FindElementByAccessibilityId("OpenNewWindowButton");
+ using var _ = newWindowButton.OpenWindowWithClick();
+
+ // Force window to front by maximizing child window.
+ Session.FindElementByAccessibilityId("CurrentWindowState").SendClick();
+ Session.FindElementByAccessibilityId("WindowStateMaximized").SendClick();
+
+ // Switch back to the mainwindow context and verify tooltip is still accessible.
+ Session.SwitchTo().Window(mainWindowHandle);
+ Assert.NotNull(Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton"));
+
+ // Verify we cannot interact with the popup by attempting closing it via button.
+ Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton").Click();
+
+ // Verify popup is still accessible.
+ Assert.NotNull(Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton"));
+ }
+ finally
+ {
+ // At this point secondary window should be already closed. And safe to close the popup.
+ Session.FindElementByAccessibilityId("StaysOpenPopupCloseButton").Click();
+ }
+ }
+
+ [Collection("Default")]
+ public class Default : PopupsTests
+ {
+ public Default(DefaultAppFixture fixture) : base(false, fixture) { }
+ }
+
+ [Collection("OverlayPopups")]
+ public class OverlayPopups : PopupsTests
+ {
+ public OverlayPopups(OverlayPopupsAppFixture fixture) : base(true, fixture) { }
+ }
+}