using System; using System.ComponentModel; using System.Linq; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.UnitTests; using Avalonia.VisualTree; using Moq; using Xunit; namespace Avalonia.Controls.UnitTests { public class FlyoutTests : ScopedTestBase { protected bool UseOverlayPopups { get; set; } [Fact] public void Opening_Raises_Single_Opening_Event() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Show(); int tracker = 0; Flyout f = new Flyout(); f.Opening += (s, e) => { tracker++; }; f.ShowAt(window); Assert.Equal(1, tracker); Assert.True(f.IsOpen); } } [Fact] public void Opening_Raises_Single_Opened_Event() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Show(); int tracker = 0; Flyout f = new Flyout(); f.Opened += (s, e) => { tracker++; }; f.ShowAt(window); Assert.Equal(1, tracker); } } [Fact] public void Opening_Is_Cancellable() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Show(); int tracker = 0; Flyout f = new Flyout(); f.Opening += (s, e) => { tracker++; if (e is CancelEventArgs cancelEventArgs) { cancelEventArgs.Cancel = true; } }; f.ShowAt(window); Assert.Equal(1, tracker); Assert.False(f.IsOpen); } } [Fact] public void Closing_Raises_Single_Closing_Event() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Show(); int tracker = 0; Flyout f = new Flyout(); f.Closing += (s, e) => { tracker++; }; f.ShowAt(window); f.Hide(); Assert.Equal(1, tracker); } } [Fact] public void Closing_Raises_Single_Closed_Event() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Show(); int tracker = 0; Flyout f = new Flyout(); f.Closed += (s, e) => { tracker++; }; f.ShowAt(window); f.Hide(); Assert.Equal(1, tracker); } } [Fact] public void Cancel_Closing_Keeps_Flyout_Open() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Show(); var tracker = 0; var f = new Flyout(); f.Closing += (s, e) => { tracker++; e.Cancel = true; }; f.ShowAt(window); f.Hide(); Assert.True(f.IsOpen); Assert.Equal(1, tracker); } } [Fact] public void Cancel_Light_Dismiss_Closing_Keeps_Flyout_Open() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Width = 100; window.Height = 100; var button = new Button { Height = 10, Width = 10, HorizontalAlignment = Layout.HorizontalAlignment.Left, VerticalAlignment = Layout.VerticalAlignment.Top }; window.Content = button; window.Show(); var tracker = 0; var f = new Flyout(); f.Content = new Border { Width = 10, Height = 10 }; f.Closing += (s, e) => { tracker++; e.Cancel = true; }; f.ShowAt(window); var e = CreatePointerPressedEventArgs(window, new Point(90, 90)); var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window); overlay.RaiseEvent(e); Assert.Equal(1, tracker); Assert.True(f.IsOpen); } } [Fact] public void Light_Dismiss_Closes_Flyout() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); window.Width = 100; window.Height = 100; var button = new Button { Height = 10, Width = 10, HorizontalAlignment = Layout.HorizontalAlignment.Left, VerticalAlignment = Layout.VerticalAlignment.Top }; window.Content = button; window.Show(); var f = new Flyout(); f.Content = new Border { Width = 10, Height = 10 }; f.ShowAt(window); var e = CreatePointerPressedEventArgs(window, new Point(90, 90)); var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window); overlay.RaiseEvent(e); 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() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); Button target1 = new Button(); Button target2 = new Button(); window.Content = new StackPanel { Children = { target1, target2 } }; window.Show(); bool closingFired = false; bool closedFired = false; Flyout f = new Flyout(); f.Closing += (s, e) => { closingFired = true; //This shouldn't happen }; f.Closed += (s, e) => { closedFired = true; }; f.ShowAt(target1); f.ShowAt(target2); Assert.False(closingFired); Assert.True(closedFired); } } [Fact] public void ShowMode_Standard_Attemps_Focus_Flyout_Content() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); var flyoutTextBox = new TextBox(); var button = new Button { Flyout = new Flyout { ShowMode = FlyoutShowMode.Standard, Content = new Panel { Children = { flyoutTextBox } } } }; window.Content = button; window.Show(); button.Focus(); Assert.Same(button, window.FocusManager!.GetFocusedElement()); button.Flyout.ShowAt(button); Assert.False(button.IsFocused); Assert.Same(flyoutTextBox, window.FocusManager!.GetFocusedElement()); } } [Fact] public void ShowMode_Transient_Does_Not_Move_Focus_From_Target() { using (CreateServicesWithFocus()) { var window = PreparedWindow(); var flyoutTextBox = new TextBox(); var button = new Button { Flyout = new Flyout { ShowMode = FlyoutShowMode.Transient, Content = new Panel { Children = { flyoutTextBox } } }, Content = "Test" }; window.Content = button; window.Show(); button.Focus(); Assert.True(window.FocusManager.GetFocusedElement() == button); button.Flyout.ShowAt(button); Assert.True(window.FocusManager.GetFocusedElement() == button); } } [Fact] public void ContextRequested_Opens_ContextFlyout() { using (CreateServicesWithFocus()) { var flyout = new Flyout(); var target = new Panel { ContextFlyout = flyout }; var window = PreparedWindow(target); window.Show(); int openedCount = 0; flyout.Opened += (sender, args) => { openedCount++; }; target.RaiseEvent(new ContextRequestedEventArgs()); Assert.True(flyout.IsOpen); Assert.Equal(1, openedCount); } } [Fact] public void KeyUp_Raised_On_Target_Opens_ContextFlyout() { using (CreateServicesWithFocus()) { var flyout = new Flyout(); var target = new Panel { ContextFlyout = flyout }; var contextRequestedCount = 0; target.AddHandler(Control.ContextRequestedEvent, (s, a) => contextRequestedCount++, Interactivity.RoutingStrategies.Tunnel); var window = PreparedWindow(target); window.Show(); target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window }); Assert.True(flyout.IsOpen); Assert.Equal(1, contextRequestedCount); } } [Fact] public void KeyUp_Raised_On_Target_Closes_Opened_ContextFlyout() { using (CreateServicesWithFocus()) { var flyout = new Flyout(); var target = new Panel { ContextFlyout = flyout }; var window = PreparedWindow(target); window.Show(); target.RaiseEvent(new ContextRequestedEventArgs()); Assert.True(flyout.IsOpen); target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window }); Assert.False(flyout.IsOpen); } } [Fact] public void KeyUp_Raised_On_Flyout_Closes_Opened_ContextFlyout() { using (CreateServicesWithFocus()) { var flyoutContent = new Button(); var flyout = new Flyout() { Content = flyoutContent }; var target = new Panel { ContextFlyout = flyout }; var window = PreparedWindow(target); window.Show(); target.RaiseEvent(new ContextRequestedEventArgs()); Assert.True(flyout.IsOpen); flyoutContent.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyUpEvent, Key = Key.Apps, Source = window }); Assert.False(flyout.IsOpen); } } [Fact] public void Should_Reset_Popup_Parent_On_Target_Detached() { using (CreateServicesWithFocus()) { var userControl = new UserControl(); var window = PreparedWindow(userControl); window.Show(); var flyout = new TestFlyout(); flyout.ShowAt(userControl); var popup = Assert.IsType(flyout.Popup); Assert.NotNull(popup.Parent); window.Content = null; Assert.Null(popup.Parent); } } [Fact] public void Should_Reset_Popup_Parent_On_Target_Attach_Following_Detach() { using (CreateServicesWithFocus()) { var userControl = new UserControl(); var window = PreparedWindow(userControl); window.Show(); var flyout = new TestFlyout(); flyout.ShowAt(userControl); var popup = Assert.IsType(flyout.Popup); Assert.NotNull(popup.Parent); flyout.Hide(); flyout.ShowAt(userControl); Assert.NotNull(popup.Parent); } } [Fact] public void ContextFlyout_Can_Be_Set_In_Styles() { using (CreateServicesWithFocus()) { var xaml = @" "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var target1 = window.Find("target1"); var target2 = window.Find("target2"); var mouse = new MouseTestHelper(); Assert.NotNull(target1.ContextFlyout); Assert.NotNull(target2.ContextFlyout); Assert.Same(target1.ContextFlyout, target2.ContextFlyout); window.Show(); var menu = target1.ContextFlyout; mouse.Click(target1, MouseButton.Right); Assert.True(menu.IsOpen); mouse.Click(target2, MouseButton.Right); Assert.True(menu.IsOpen); } } [Fact] public void Setting_FlyoutPresenterClasses_Sets_Classes_On_FlyoutPresenter() { using (CreateServicesWithFocus()) { var xaml = @" "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var flyoutPanel = new Panel(); var button = new Button { Content = "Test", Flyout = new Flyout { Content = flyoutPanel } }; window.Content = button; window.Show(); (button.Flyout as Flyout).FlyoutPresenterClasses.Add("TestClass"); button.Flyout.ShowAt(button); var presenter = flyoutPanel.GetVisualAncestors().OfType().FirstOrDefault(); Assert.NotNull(presenter); Assert.True((presenter.Background as ISolidColorBrush).Color == Colors.Red); } } private IDisposable CreateServicesWithFocus() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: new MockWindowingPlatform(null, x => UseOverlayPopups ? null : MockWindowingPlatform.CreatePopupMock(x).Object), focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice())); } private static Window PreparedWindow(object content = null) { var platform = AvaloniaLocator.Current.GetRequiredService(); var windowImpl = Mock.Get(platform.CreateWindow()); windowImpl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor()); var w = new Window(windowImpl.Object) { Content = content }; w.ApplyTemplate(); return w; } private static PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p) { var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); return new PointerPressedEventArgs( source, pointer, source, p, 0, new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), KeyModifiers.None); } public class TestFlyout : Flyout { public new Popup Popup => base.Popup; } } public class OverlayPopupFlyoutTests : FlyoutTests { public OverlayPopupFlyoutTests() => UseOverlayPopups = true; } }