浏览代码

Merge pull request #5682 from amwx/Flyouts

Implement Flyouts
Dan Walmsley 4 年之前
父节点
当前提交
cf5ce93cba
共有 28 个文件被更改,包括 1991 次插入4 次删除
  1. 6 0
      samples/ControlCatalog/MainView.xaml
  2. 102 0
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml
  3. 45 0
      samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs
  4. 264 0
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  5. 81 0
      samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs
  6. 78 0
      samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs
  7. 38 0
      src/Avalonia.Controls/Button.cs
  8. 22 0
      src/Avalonia.Controls/Control.cs
  9. 50 0
      src/Avalonia.Controls/Flyouts/Flyout.cs
  10. 504 0
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  11. 77 0
      src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs
  12. 33 0
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  13. 24 0
      src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs
  14. 75 0
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  15. 55 0
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  16. 2 1
      src/Avalonia.Controls/TextBox.cs
  17. 2 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  18. 32 0
      src/Avalonia.Themes.Default/FlyoutPresenter.xaml
  19. 29 0
      src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml
  20. 9 1
      src/Avalonia.Themes.Default/TextBox.xaml
  21. 5 0
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
  22. 5 0
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
  23. 2 0
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  24. 43 0
      src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml
  25. 35 0
      src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml
  26. 9 1
      src/Avalonia.Themes.Fluent/Controls/TextBox.xaml
  27. 325 0
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs
  28. 39 1
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

+ 6 - 0
samples/ControlCatalog/MainView.xaml

@@ -21,6 +21,9 @@
       <TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
       <TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
       <TabItem Header="ComboBox"><pages:ComboBoxPage/></TabItem>
+      <TabItem Header="ContextFlyout">
+        <pages:ContextFlyoutPage/>
+      </TabItem>
       <TabItem Header="ContextMenu"><pages:ContextMenuPage/></TabItem>
       <TabItem Header="Cursor"
                ScrollViewer.VerticalScrollBarVisibility="Disabled">
@@ -38,6 +41,9 @@
         <pages:CalendarDatePickerPage/></TabItem>
       <TabItem Header="Drag+Drop"><pages:DragAndDropPage/></TabItem>
       <TabItem Header="Expander"><pages:ExpanderPage/></TabItem>
+      <TabItem Header="Flyouts">
+        <pages:FlyoutsPage />
+      </TabItem>
       <TabItem Header="Image"
                ScrollViewer.VerticalScrollBarVisibility="Disabled"
                ScrollViewer.HorizontalScrollBarVisibility="Disabled">

+ 102 - 0
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml

@@ -0,0 +1,102 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="ControlCatalog.Pages.ContextFlyoutPage">
+    <UserControl.Styles>
+        <Style Selector="FlyoutPresenter.NoPadding">
+            <Setter Property="Padding" Value="0" />
+        </Style>
+    </UserControl.Styles>
+    
+    <StackPanel Orientation="Vertical" Spacing="4">
+        <TextBlock Classes="h1">Context Flyout</TextBlock>
+        <TextBlock Classes="h2">A right click Flyout that can be applied to any control.</TextBlock>
+
+        <StackPanel Orientation="Horizontal"
+              Margin="0,16,0,0"
+              HorizontalAlignment="Center"
+              Spacing="16">
+            <Border Background="{DynamicResource SystemAccentColor}"
+                    Margin="16"
+                    Padding="48,48,48,48">
+                <Border.ContextFlyout>
+                    <MenuFlyout>
+                        <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
+                        <MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
+                        <Separator/>
+                        <MenuItem Header="Menu with _Submenu">
+                            <MenuItem Header="Submenu _1"/>
+                            <MenuItem Header="Submenu _2"/>
+                        </MenuItem>
+                        <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
+                            <MenuItem.Icon>
+                                <Image Source="/Assets/github_icon.png"/>
+                            </MenuItem.Icon>
+                        </MenuItem>
+                        <MenuItem Header="Menu Item with _Checkbox">
+                            <MenuItem.Icon>
+                                <CheckBox BorderThickness="0" IsHitTestVisible="False" IsChecked="True"/>
+                            </MenuItem.Icon>
+                        </MenuItem>
+                    </MenuFlyout>
+                </Border.ContextFlyout>
+                <TextBlock Text="Defined in XAML"/>
+            </Border>
+            <Border Background="{DynamicResource SystemAccentColor}"
+                    Margin="16"
+                    Padding="48,48,48,48">
+                <Border.ContextMenu>
+                    <ContextMenu Items="{Binding MenuItems}">
+                        <ContextMenu.Styles>
+                            <Style Selector="MenuItem">
+                                <Setter Property="Header" Value="{Binding Header}"/>
+                                <Setter Property="Items" Value="{Binding Items}"/>
+                                <Setter Property="Command" Value="{Binding Command}"/>
+                                <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
+                            </Style>
+                        </ContextMenu.Styles>
+                    </ContextMenu>
+                </Border.ContextMenu>
+                <TextBlock Text="Dynamically Generated"/>
+            </Border>
+        </StackPanel>
+
+        <TextBlock Text="Custom ContextFlyout for TextBox" />
+
+        <TextBox Name="TextBox" Width="150" HorizontalAlignment="Center" ContextMenu="{x:Null}">
+            <TextBox.ContextFlyout>
+                <Flyout FlyoutPresenterClasses="NoPadding">
+                    <StackPanel Orientation="Horizontal">
+                        <StackPanel.Styles>
+                            <Style Selector="Button">
+                                <Setter Property="Background" Value="Transparent" />
+                                <Setter Property="Height" Value="40" />
+                                <Setter Property="Width" Value="40" />
+                                <Setter Property="VerticalContentAlignment" Value="Center" />
+                            </Style>
+                            <Style Selector="Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
+                                <Setter Property="Background" Value="Transparent" />
+                                <Setter Property="Opacity" Value="0.5" />
+                            </Style>
+                        </StackPanel.Styles>
+                        <Button Name="CutButton" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}">
+                            <PathIcon Width="14" Height="14" Data="M5.22774,2.08072 C5.43359778,1.94704 5.7011484,1.98419259 5.86368634,2.15675215 L5.91939,2.22774 L12.5191,12.3904 C12.956,12.1419 13.4614,12.0000019 14,12.0000019 C15.6569,12.0000019 17,13.3431 17,15.0000019 C17,16.6569 15.6569,18.0000019 14,18.0000019 C12.3431,18.0000019 11,16.6569 11,15.0000019 C11,14.3201402 11.226152,13.693011 11.6073785,13.1899092 L11.7401,13.0269 L10,10.3474 L8.25991,13.0269 C8.72078,13.5543 9,14.2446 9,15.0000019 C9,16.6569 7.65685,18.0000019 6,18.0000019 C4.34315,18.0000019 3,16.6569 3,15.0000019 C3,13.3431 4.34315,12.0000019 6,12.0000019 C6.46163143,12.0000019 6.89890041,12.1042536 7.28955831,12.2905296 L7.4809,12.3904 L9.40382,9.42936 L5.08072,2.77238 C4.93033,2.54079 4.99615,2.23112 5.22774,2.08072 Z M14,13 C12.8954,13 12,13.8954 12,15 C12,16.1046 12.8954,17 14,17 C15.1046,17 16,16.1046 16,15 C16,13.8954 15.1046,13 14,13 Z M6,13 C4.89543,13 4,13.8954 4,15 C4,16.1046 4.89543,17 6,17 C7.10457,17 8,16.1046 8,15 C8,13.8954 7.10457,13 6,13 Z M14.7723,2.08072 C15.0039,2.23112 15.0697,2.54079 14.9193,2.77238 L11.1924,8.51133 L10.5962,7.59329 L14.0806,2.22774 C14.231,1.99615 14.5407,1.93033 14.7723,2.08072 Z" />
+                        </Button>
+                        <Button Name="CopyButton" Content="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}">
+                            <PathIcon Width="14" Height="14" Data="M5.50280381,4.62704038 L5.5,6.75 L5.5,17.2542087 C5.5,19.0491342 6.95507456,20.5042087 8.75,20.5042087 L17.3662868,20.5044622 C17.057338,21.3782241 16.2239751,22.0042087 15.2444057,22.0042087 L8.75,22.0042087 C6.12664744,22.0042087 4,19.8775613 4,17.2542087 L4,6.75 C4,5.76928848 4.62744523,4.93512464 5.50280381,4.62704038 Z M17.75,2 C18.9926407,2 20,3.00735931 20,4.25 L20,17.25 C20,18.4926407 18.9926407,19.5 17.75,19.5 L8.75,19.5 C7.50735931,19.5 6.5,18.4926407 6.5,17.25 L6.5,4.25 C6.5,3.00735931 7.50735931,2 8.75,2 L17.75,2 Z M17.75,3.5 L8.75,3.5 C8.33578644,3.5 8,3.83578644 8,4.25 L8,17.25 C8,17.6642136 8.33578644,18 8.75,18 L17.75,18 C18.1642136,18 18.5,17.6642136 18.5,17.25 L18.5,4.25 C18.5,3.83578644 18.1642136,3.5 17.75,3.5 Z" />
+                        </Button>
+                        <Button Name="PasteButton" Content="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}">
+                            <PathIcon Width="14" Height="14" Data="M13.75,2 C14.940864,2 15.9156449,2.92516159 15.9948092,4.09595119 L16,4.25 L16,4.25 C16,4.16530567 15.9953205,4.0817043 15.9862059,3.99944035 L17.75,4 C18.9926407,4 20,5.00735931 20,6.25 L20,19.75 C20,20.9926407 18.9926407,22 17.75,22 L6.25,22 C5.00735931,22 4,20.9926407 4,19.75 L4,6.25 C4,5.00735931 5.00735931,4 6.25,4 L8.01379413,3.99944035 C8.00733496,4.05773764 8.00310309,4.11670658 8.00118552,4.17626017 L8,4.25 C8,3.00735931 9.00735931,2 10.25,2 L13.75,2 Z M13.75,6.5 L10.25,6.5 C9.45594921,6.5 8.75796956,6.08867052 8.357512,5.4674625 L8.37902077,5.50019943 L8.37902077,5.50019943 L6.25,5.5 C5.83578644,5.5 5.5,5.83578644 5.5,6.25 L5.5,19.75 C5.5,20.1642136 5.83578644,20.5 6.25,20.5 L17.75,20.5 C18.1642136,20.5 18.5,20.1642136 18.5,19.75 L18.5,6.25 C18.5,5.83578644 18.1642136,5.5 17.75,5.5 L15.6209792,5.50019943 L15.642488,5.4674625 C15.2420304,6.08867052 14.5440508,6.5 13.75,6.5 Z M13.75,3.5 L10.25,3.5 C9.83578644,3.5 9.5,3.83578644 9.5,4.25 C9.5,4.66421356 9.83578644,5 10.25,5 L13.75,5 C14.1642136,5 14.5,4.66421356 14.5,4.25 C14.5,3.83578644 14.1642136,3.5 13.75,3.5 Z" />
+                        </Button>
+                        <Button Name="ClearButton" Content="Clear" Command="{Binding $parent[TextBox].Clear}">
+                            <PathIcon Width="14" Height="14" Data="M3.52499419,3.71761187 L3.61611652,3.61611652 C4.0717282,3.16050485 4.79154862,3.13013074 5.28238813,3.52499419 L5.38388348,3.61611652 L14,12.233 L22.6161165,3.61611652 C23.1042719,3.12796116 23.8957281,3.12796116 24.3838835,3.61611652 C24.8720388,4.10427189 24.8720388,4.89572811 24.3838835,5.38388348 L15.767,14 L24.3838835,22.6161165 C24.8394952,23.0717282 24.8698693,23.7915486 24.4750058,24.2823881 L24.3838835,24.3838835 C23.9282718,24.8394952 23.2084514,24.8698693 22.7176119,24.4750058 L22.6161165,24.3838835 L14,15.767 L5.38388348,24.3838835 C4.89572811,24.8720388 4.10427189,24.8720388 3.61611652,24.3838835 C3.12796116,23.8957281 3.12796116,23.1042719 3.61611652,22.6161165 L12.233,14 L3.61611652,5.38388348 C3.16050485,4.9282718 3.13013074,4.20845138 3.52499419,3.71761187 L3.61611652,3.61611652 L3.52499419,3.71761187 Z" />
+                        </Button>
+                    </StackPanel>
+                </Flyout>
+            </TextBox.ContextFlyout>
+        </TextBox>
+    
+    </StackPanel>
+</UserControl>

+ 45 - 0
samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs

@@ -0,0 +1,45 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+using Avalonia.Interactivity;
+namespace ControlCatalog.Pages
+{
+    public class ContextFlyoutPage : UserControl
+    {
+        private TextBox _textBox;
+
+        public ContextFlyoutPage()
+        {
+            InitializeComponent();
+
+            var vm = new ContextFlyoutPageViewModel();
+            vm.View = this;
+            DataContext = vm;
+
+            _textBox = this.FindControl<TextBox>("TextBox");
+
+            var cutButton = this.FindControl<Button>("CutButton");
+            cutButton.Click += CloseFlyout;
+
+            var copyButton = this.FindControl<Button>("CopyButton");
+            copyButton.Click += CloseFlyout;
+
+            var pasteButton = this.FindControl<Button>("PasteButton");
+            pasteButton.Click += CloseFlyout;
+
+            var clearButton = this.FindControl<Button>("ClearButton");
+            clearButton.Click += CloseFlyout;
+        }
+
+        private void CloseFlyout(object sender, RoutedEventArgs e)
+        {
+            _textBox.ContextFlyout.Hide();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 264 - 0
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@@ -0,0 +1,264 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="700"
+             x:Class="ControlCatalog.Pages.FlyoutsPage">
+
+    <UserControl.Resources>
+        <MenuFlyout x:Key="SharedMenuFlyout">
+            <MenuItem Header="Item 1">
+                <MenuItem Header="Subitem 1" />
+                <MenuItem Header="Subitem 2" />
+                <MenuItem Header="Subitem 3" />
+            </MenuItem>
+            <MenuItem Header="Item 2" InputGesture="Ctrl+A" />
+            <MenuItem Header="Item 3" />
+        </MenuFlyout>
+        <Flyout Placement="Bottom" x:Key="BasicFlyout">
+            <Panel Width="100" Height="100">
+                <TextBlock Text="Flyout Content!" />
+            </Panel>
+        </Flyout>
+    </UserControl.Resources>
+
+    <ScrollViewer HorizontalScrollBarVisibility="Disabled">
+        <StackPanel Spacing="10">
+            <TextBlock FontSize="18" Text="Button with a Flyout" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <Button Content="Click Me!" Flyout="{StaticResource BasicFlyout}" />
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="ButtonFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="MenuFlyout" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <Button Content="Click Me!" Flyout="{StaticResource SharedMenuFlyout}" />
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="MenuFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="Attached Flyouts" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}"
+                           HorizontalAlignment="Left"
+                           Height="100"
+                           Name="AttachedFlyoutPanel">
+                        <FlyoutBase.AttachedFlyout>
+                            <Flyout>
+                                <Panel Height="100">
+                                    <TextBlock Text="Attached Flyout!"
+                                               VerticalAlignment="Center"
+                                               Margin="10"/>
+                                </Panel>
+                            </Flyout>
+                        </FlyoutBase.AttachedFlyout>
+
+                        <TextBlock Text="Double click panel to launch AttachedFlyout"
+                                   VerticalAlignment="Center"
+                                   Margin="10"/>
+
+                    </Panel>
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="AttachedFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+
+            <TextBlock FontSize="18" Text="Sharing Flyouts" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <StackPanel Orientation="Horizontal" Spacing="30">
+                        <Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
+                        <Button Content="Launch Flyout on this button" Flyout="{StaticResource SharedMenuFlyout}"/>
+                    </StackPanel>
+                </Border>
+                <Panel Background="{DynamicResource SystemControlBackgroundBaseLowBrush}">
+                    <TextBlock Name="SharedFlyoutXamlText" Padding="15" />
+                </Panel>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="Flyout Placements" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <UniformGrid Columns="3">
+                        <UniformGrid.Styles>
+                            <Style Selector="Button">
+                                <Setter Property="Margin" Value="10" />
+                            </Style>
+                        </UniformGrid.Styles>
+                        <Button Content="Placement=Top">
+                            <Button.Flyout>
+                                <Flyout Placement="Top">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=Bottom">
+                            <Button.Flyout>
+                                <Flyout Placement="Bottom">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=Left">
+                            <Button.Flyout>
+                                <Flyout Placement="Left">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=Right">
+                            <Button.Flyout>
+                                <Flyout Placement="Right">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=TopEdgeAlignedLeft">
+                            <Button.Flyout>
+                                <Flyout Placement="TopEdgeAlignedLeft">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=TopEdgeAlignedRight">
+                            <Button.Flyout>
+                                <Flyout Placement="TopEdgeAlignedRight">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=BottomEdgeAlignedLeft">
+                            <Button.Flyout>
+                                <Flyout Placement="BottomEdgeAlignedLeft">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=BottomEdgeAlignedRight">
+                            <Button.Flyout>
+                                <Flyout Placement="BottomEdgeAlignedRight">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=LeftEdgeAlignedTop">
+                            <Button.Flyout>
+                                <Flyout Placement="LeftEdgeAlignedTop">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=LeftEdgeAlignedBottom">
+                            <Button.Flyout>
+                                <Flyout Placement="LeftEdgeAlignedBottom">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=RightEdgeAlignedBottom">
+                            <Button.Flyout>
+                                <Flyout Placement="RightEdgeAlignedTop">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="Placement=RightEdgeAlignedBottom">
+                            <Button.Flyout>
+                                <Flyout Placement="RightEdgeAlignedBottom">
+                                    <Panel Width="100" Height="100">
+                                        <TextBlock Text="Flyout Content!" />
+                                    </Panel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+
+                    </UniformGrid>
+                </Border>
+            </StackPanel>
+
+            <TextBlock FontSize="18" Text="Flyout ShowMode" />
+            <StackPanel>
+                <Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}"
+                        BorderThickness="1" Padding="15">
+                    <WrapPanel Orientation="Horizontal">
+                        <WrapPanel.Styles>
+                            <Style Selector="Button">
+                                <Setter Property="Margin" Value="4" />
+                            </Style>
+                        </WrapPanel.Styles>
+                        <Button Content="ShowMode=Standard (default)">
+                            <Button.Flyout>
+                                <Flyout>
+                                    <StackPanel Width="200">
+                                        <TextBox />
+                                        <TextBlock Text="Standard ShowMode attempts to focus the Flyout when its opened" TextWrapping="Wrap"/>
+                                    </StackPanel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="ShowMode=Transient">
+                            <Button.Flyout>
+                                <Flyout ShowMode="Transient">
+                                    <StackPanel Width="200">
+                                        <TextBox />
+                                        <TextBlock Text="Transient ShowMode does not focus the Flyout when opened" TextWrapping="Wrap"/>
+                                    </StackPanel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        <Button Content="ShowMode=TransientWithDismissOnPointerMoveAway">
+                            <Button.Flyout>
+                                <Flyout ShowMode="TransientWithDismissOnPointerMoveAway">
+                                    <StackPanel Width="200">
+                                        <TextBox />
+                                        <TextBlock Text="Show in Transient mode (no focus), but closes the Flyout when the pointer moves away" TextWrapping="Wrap"/>
+                                    </StackPanel>
+                                </Flyout>
+                            </Button.Flyout>
+                        </Button>
+                        
+                    </WrapPanel>
+                </Border>
+            </StackPanel>
+            
+        </StackPanel>
+    </ScrollViewer>
+    
+</UserControl>

+ 81 - 0
samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs

@@ -0,0 +1,81 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Markup.Xaml;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+    public class FlyoutsPage : UserControl
+    {
+        public FlyoutsPage()
+        {
+            InitializeComponent();
+
+            var afp = this.FindControl<Panel>("AttachedFlyoutPanel");
+            if (afp != null)
+            {
+                afp.DoubleTapped += Afp_DoubleTapped;
+            }
+
+            SetXamlTexts();
+        }
+
+        private void Afp_DoubleTapped(object sender, RoutedEventArgs e)
+        {
+            if (sender is Panel p)
+            {
+                FlyoutBase.ShowAttachedFlyout(p);
+            }
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        private void SetXamlTexts()
+        {
+            var bfxt = this.FindControl<TextBlock>("ButtonFlyoutXamlText");
+            bfxt.Text = "<Button Content=\"Click me!\">\n" +
+                        "    <Button.Flyout>\n" +
+                        "        <Flyout>\n" +
+                        "            <Panel Width=\"100\" Height=\"100\">\n" +
+                        "                <TextBlock Text=\"Flyout Content!\" />\n" +
+                        "            </Panel>\n" +
+                        "        </Flyout>\n" +
+                        "    </Button.Flyout>\n</Button>";
+
+            var mfxt = this.FindControl<TextBlock>("MenuFlyoutXamlText");
+            mfxt.Text = "<Button Content=\"Click me!\">\n" +
+                    "    <Button.Flyout>\n" +
+                    "        <MenuFlyout>\n" +
+                    "            <MenuItem Header=\"Item 1\">\n" +
+                    "            <MenuItem Header=\"Item 2\">\n" +
+                    "        </MenuFlyout>\n" +
+                    "    </Button.Flyout>\n</Button>";
+
+            var afxt = this.FindControl<TextBlock>("AttachedFlyoutXamlText");
+            afxt.Text = "<Panel Name=\"AttachedFlyoutPanel\">\n" +
+                "    <FlyoutBase.AttachedFlyout>\n" +
+                "        <Flyout>\n" +
+                "            <Panel Height=\"100\">\n" +
+                "                <TextBlock Text=\"Attached Flyout\" />\n" +
+                "            </Panel>\n" +
+                "        </Flyout>\n" +
+                "    </FlyoutBase.AttachedFlyout>\n</Panel>" + 
+                "\n\n In DoubleTapped handler:\n" +
+                "FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);";
+
+            var sfxt = this.FindControl<TextBlock>("SharedFlyoutXamlText");
+            sfxt.Text = "Declare a flyout in Resources:\n" +
+                "<Window.Resources>\n" +
+                "    <Flyout x:Key=\"SharedFlyout\">\n" +
+                "        <Panel Width=\"100\" Height=\"100\">\n" +
+                "            <TextBlock Text=\"Flyout Content!\" />\n" +
+                "        </Panel>\n" +
+                "    </Flyout>\n</Window.Resources>\n\n" +
+                "Then attach the flyout where you want it:\n" +
+                "<Button Content=\"Launch Flyout here\" Flyout=\"{StaticResource SharedFlyout}\" />";
+        }
+    }
+}

+ 78 - 0
samples/ControlCatalog/ViewModels/ContextFlyoutPageViewModel.cs

@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.Reactive;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.VisualTree;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+    public class ContextFlyoutPageViewModel
+    {
+        public Control View { get; set; }
+        public ContextFlyoutPageViewModel()
+        {
+            OpenCommand = MiniCommand.CreateFromTask(Open);
+            SaveCommand = MiniCommand.Create(Save);
+            OpenRecentCommand = MiniCommand.Create<string>(OpenRecent);
+
+            MenuItems = new[]
+            {
+                new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
+                new MenuItemViewModel { Header = "Save", Command = SaveCommand },
+                new MenuItemViewModel { Header = "-" },
+                new MenuItemViewModel
+                {
+                    Header = "Recent",
+                    Items = new[]
+                    {
+                        new MenuItemViewModel
+                        {
+                            Header = "File1.txt",
+                            Command = OpenRecentCommand,
+                            CommandParameter = @"c:\foo\File1.txt"
+                        },
+                        new MenuItemViewModel
+                        {
+                            Header = "File2.txt",
+                            Command = OpenRecentCommand,
+                            CommandParameter = @"c:\foo\File2.txt"
+                        },
+                    }
+                },
+            };
+        }
+
+        public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
+        public MiniCommand OpenCommand { get; }
+        public MiniCommand SaveCommand { get; }
+        public MiniCommand OpenRecentCommand { get; }
+
+        public async Task Open()
+        {
+            var window = View?.GetVisualRoot() as Window;
+            if (window == null)
+                return;
+            var dialog = new OpenFileDialog();
+            var result = await dialog.ShowAsync(window);
+
+            if (result != null)
+            {
+                foreach (var path in result)
+                {
+                    System.Diagnostics.Debug.WriteLine($"Opened: {path}");
+                }
+            }
+        }
+
+        public void Save()
+        {
+            System.Diagnostics.Debug.WriteLine("Save");
+        }
+
+        public void OpenRecent(string path)
+        {
+            System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
+        }
+    }
+}

+ 38 - 0
src/Avalonia.Controls/Button.cs

@@ -2,6 +2,7 @@ using System;
 using System.Linq;
 using System.Windows.Input;
 using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Primitives;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
@@ -78,6 +79,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> IsPressedProperty =
             AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
 
+        /// <summary>
+        /// Defines the <see cref="Flyout"/> property
+        /// </summary>
+        public static readonly StyledProperty<FlyoutBase> FlyoutProperty =
+            AvaloniaProperty.Register<Button, FlyoutBase>(nameof(Flyout));
+
         private ICommand _command;
         private bool _commandCanExecute = true;
         private KeyGesture _hotkey;
@@ -169,6 +176,15 @@ namespace Avalonia.Controls
             private set { SetValue(IsPressedProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the Flyout that should be shown with this button
+        /// </summary>
+        public FlyoutBase Flyout
+        {
+            get => GetValue(FlyoutProperty);
+            set => SetValue(FlyoutProperty, value);
+        }
+
         protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; 
 
         /// <inheritdoc/>
@@ -256,6 +272,11 @@ namespace Avalonia.Controls
                 IsPressed = true;
                 e.Handled = true;
             }
+            else if (e.Key == Key.Escape && Flyout != null)
+            {
+                // If Flyout doesn't have focusable content, close the flyout here
+                Flyout.Hide();
+            }
 
             base.OnKeyDown(e);
         }
@@ -279,6 +300,8 @@ namespace Avalonia.Controls
         /// </summary>
         protected virtual void OnClick()
         {
+            OpenFlyout();
+
             var e = new RoutedEventArgs(ClickEvent);
             RaiseEvent(e);
 
@@ -289,6 +312,11 @@ namespace Avalonia.Controls
             }
         }
 
+        protected virtual void OpenFlyout()
+        {
+            Flyout?.ShowAt(this);
+        }
+
         /// <inheritdoc/>
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
@@ -337,6 +365,16 @@ namespace Avalonia.Controls
             {
                 UpdatePseudoClasses(change.NewValue.GetValueOrDefault<bool>());
             }
+            else if (change.Property == FlyoutProperty)
+            {
+                // If flyout is changed while one is already open, make sure we 
+                // close the old one first
+                if (change.OldValue.GetValueOrDefault() is FlyoutBase oldFlyout &&
+                    oldFlyout.IsOpen)
+                {
+                    oldFlyout.Hide();
+                }
+            }
         }
 
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)

+ 22 - 0
src/Avalonia.Controls/Control.cs

@@ -37,9 +37,16 @@ namespace Avalonia.Controls
         /// <summary>
         /// Defines the <see cref="ContextMenu"/> property.
         /// </summary>
+        [Obsolete("Prefer ContextFlyout")]
         public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
             AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
 
+        /// <summary>
+        /// Defines the <see cref="ContextFlyout"/> property
+        /// </summary>
+        public static readonly StyledProperty<FlyoutBase?> ContextFlyoutProperty =
+            AvaloniaProperty.Register<Control, FlyoutBase?>(nameof(ContextFlyout));
+
         /// <summary>
         /// Event raised when an element wishes to be scrolled into view.
         /// </summary>
@@ -70,12 +77,22 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets a context menu to the control.
         /// </summary>
+        [Obsolete("Prefer ContextFlyout")]
         public ContextMenu? ContextMenu
         {
             get => GetValue(ContextMenuProperty);
             set => SetValue(ContextMenuProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets a context flyout to the control
+        /// </summary>
+        public FlyoutBase? ContextFlyout
+        {
+            get => GetValue(ContextFlyoutProperty);
+            set => SetValue(ContextFlyoutProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets a user-defined object attached to the control.
         /// </summary>
@@ -93,6 +110,11 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         void ISetterValue.Initialize(ISetter setter)
         {
+            if (setter is Setter s && s.Property == ContextFlyoutProperty)
+            {
+                return; // Allow ContextFlyout to not need wrapping in <Template>
+            }
+
             throw new InvalidOperationException(
                 "Cannot use a control as a Setter value. Wrap the control in a <Template>.");
         }

+ 50 - 0
src/Avalonia.Controls/Flyouts/Flyout.cs

@@ -0,0 +1,50 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Metadata;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+    public class Flyout : FlyoutBase
+    {
+        /// <summary>
+        /// Defines the <see cref="Content"/> property
+        /// </summary>
+        public static readonly StyledProperty<object> ContentProperty =
+            AvaloniaProperty.Register<Flyout, object>(nameof(Content));
+
+        /// <summary>
+        /// Gets the Classes collection to apply to the FlyoutPresenter this Flyout is hosting
+        /// </summary>
+        public Classes FlyoutPresenterClasses => _classes ??= new Classes();
+
+        private Classes? _classes;
+
+        /// <summary>
+        /// Gets or sets the content to display in this flyout
+        /// </summary>
+        [Content]
+        public object Content
+        {
+            get => GetValue(ContentProperty);
+            set => SetValue(ContentProperty, value);
+        }
+
+        protected override Control CreatePresenter()
+        {
+            return new FlyoutPresenter
+            {
+                [!ContentControl.ContentProperty] = this[!ContentProperty]
+            };
+        }
+
+        protected override void OnOpened()
+        {
+            if (_classes != null)
+            {
+                SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
+            }
+            base.OnOpened();
+        }
+    }
+}

+ 504 - 0
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@@ -0,0 +1,504 @@
+using System;
+using System.ComponentModel;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Layout;
+using Avalonia.Logging;
+
+#nullable enable
+
+namespace Avalonia.Controls.Primitives
+{
+    public abstract class FlyoutBase : AvaloniaObject
+    {
+        static FlyoutBase()
+        {
+            Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
+        }
+
+        /// <summary>
+        /// Defines the <see cref="IsOpen"/> property
+        /// </summary>
+        private static readonly DirectProperty<FlyoutBase, bool> IsOpenProperty =
+           AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen),
+               x => x.IsOpen);
+
+        /// <summary>
+        /// Defines the <see cref="Target"/> property
+        /// </summary>
+        public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty =
+            AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target);
+
+        /// <summary>
+        /// Defines the <see cref="Placement"/> property
+        /// </summary>
+        public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
+            AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
+
+        /// <summary>
+        /// Defines the <see cref="ShowMode"/> property
+        /// </summary>
+        public static readonly DirectProperty<FlyoutBase, FlyoutShowMode> ShowModeProperty =
+            AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutShowMode>(nameof(ShowMode),
+                x => x.ShowMode, (x, v) => x.ShowMode = v);
+
+        /// <summary>
+        /// Defines the AttachedFlyout property
+        /// </summary>
+        public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
+            AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
+
+        private bool _isOpen;
+        private Control? _target;
+        private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
+        private Rect? enlargedPopupRect;
+        private IDisposable? transientDisposable;
+
+        protected Popup? Popup { get; private set; }
+
+        /// <summary>
+        /// Gets whether this Flyout is currently Open
+        /// </summary>
+        public bool IsOpen
+        {
+            get => _isOpen;
+            private set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the desired placement
+        /// </summary>
+        public FlyoutPlacementMode Placement
+        {
+            get => GetValue(PlacementProperty);
+            set => SetValue(PlacementProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the desired ShowMode
+        /// </summary>
+        public FlyoutShowMode ShowMode
+        {
+            get => _showMode;
+            set => SetAndRaise(ShowModeProperty, ref _showMode, value);
+        }
+
+        /// <summary>
+        /// Gets the Target used for showing the Flyout
+        /// </summary>
+        public Control? Target
+        {
+            get => _target;
+            private set => SetAndRaise(TargetProperty, ref _target, value);
+        }
+
+        public event EventHandler? Closed;
+        public event EventHandler<CancelEventArgs>? Closing;
+        public event EventHandler? Opened;
+        public event EventHandler? Opening;
+
+        public static FlyoutBase? GetAttachedFlyout(Control element)
+        {
+            return element.GetValue(AttachedFlyoutProperty);
+        }
+
+        public static void SetAttachedFlyout(Control element, FlyoutBase? value)
+        {
+            element.SetValue(AttachedFlyoutProperty, value);
+        }
+
+        public static void ShowAttachedFlyout(Control flyoutOwner)
+        {
+            var flyout = GetAttachedFlyout(flyoutOwner);
+            flyout?.ShowAt(flyoutOwner);
+        }
+
+        /// <summary>
+        /// Shows the Flyout at the given Control
+        /// </summary>
+        /// <param name="placementTarget">The control to show the Flyout at</param>
+        public void ShowAt(Control placementTarget)
+        {
+            ShowAtCore(placementTarget);
+        }
+
+        /// <summary>
+        /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
+        /// </summary>
+        /// <param name="placementTarget">The target control</param>
+        /// <param name="showAtPointer">True to show at pointer</param>
+        public void ShowAt(Control placementTarget, bool showAtPointer)
+        {
+            ShowAtCore(placementTarget, showAtPointer);
+        }
+
+        /// <summary>
+        /// Hides the Flyout
+        /// </summary>
+        public void Hide()
+        {
+            HideCore();
+        }
+
+        protected virtual void HideCore(bool canCancel = true)
+        {
+            if (!IsOpen)
+            {
+                return;
+            }
+
+            if (canCancel)
+            {
+                bool cancel = false;
+
+                var closing = new CancelEventArgs();
+                Closing?.Invoke(this, closing);
+                if (cancel || closing.Cancel)
+                {
+                    return;
+                }
+            }
+
+            IsOpen = false;
+            Popup.IsOpen = false;
+
+            // Ensure this isn't active
+            transientDisposable?.Dispose();
+            transientDisposable = null;
+
+            OnClosed();
+        }
+
+        protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false)
+        {
+            if (placementTarget == null)
+                throw new ArgumentNullException("placementTarget cannot be null");
+
+            if (Popup == null)
+            {
+                InitPopup();
+            }
+
+            if (IsOpen)
+            {
+                if (placementTarget == Target)
+                {
+                    return;
+                }
+                else // Close before opening a new one
+                {
+                    HideCore(false);
+                }
+            }
+
+            if (Popup.Parent != null && Popup.Parent != placementTarget)
+            {
+                ((ISetLogicalParent)Popup).SetParent(null);
+            }
+
+            if (Popup.PlacementTarget != placementTarget)
+            {
+                Popup.PlacementTarget = Target = placementTarget;
+                ((ISetLogicalParent)Popup).SetParent(placementTarget);
+            }
+
+            if (Popup.Child == null)
+            {
+                Popup.Child = CreatePresenter();
+            }
+
+            OnOpening();
+            PositionPopup(showAtPointer);
+            IsOpen = Popup.IsOpen = true;            
+            OnOpened();
+                        
+            if (ShowMode == FlyoutShowMode.Standard)
+            {
+                // Try and focus content inside Flyout
+                if (Popup.Child.Focusable)
+                {
+                    FocusManager.Instance?.Focus(Popup.Child);
+                }
+                else
+                {
+                    var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
+                    if (nextFocus != null)
+                    {
+                        FocusManager.Instance?.Focus(nextFocus);
+                    }
+                }
+            }
+            else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
+            {
+                transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
+            }
+        }
+
+        private void HandleTransientDismiss(RawInputEventArgs args)
+        {
+            if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
+            {
+                // In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
+                // shown as long as the pointer is within a certain px distance from the
+                // flyout itself. I'm not sure what WinUI uses, but I'm defaulting to 
+                // 100px, which seems about right
+                // enlargedPopupRect is the Flyout bounds enlarged 100px
+                // For windowed popups, enlargedPopupRect is in screen coordinates,
+                // for overlay popups, its in OverlayLayer coordinates
+
+                if (enlargedPopupRect == null)
+                {
+                    // Only do this once when the Flyout opens & cache the result
+                    if (Popup?.Host is PopupRoot root)
+                    { 
+                        // Get the popup root bounds and convert to screen coordinates
+                        var tmp = root.Bounds.Inflate(100);
+                        var scPt = root.PointToScreen(tmp.TopLeft);
+                        enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height);
+                    }
+                    else if (Popup?.Host is OverlayPopupHost host)
+                    {
+                        // Overlay popups are in OverlayLayer coordinates, just use that
+                        enlargedPopupRect = host.Bounds.Inflate(100);
+                    }
+
+                    return;
+                }
+
+                if (Popup?.Host is PopupRoot)
+                {
+                    // As long as the pointer stays within the enlargedPopupRect
+                    // the flyout stays open. If it leaves, close it
+                    // Despite working in screen coordinates, leaving the TopLevel
+                    // window will not close this (as pointer events stop), which 
+                    // does match UWP
+                    var pt = pArgs.Root.PointToScreen(pArgs.Position);
+                    if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false)
+                    {
+                        HideCore(false);
+                        enlargedPopupRect = null;
+                        transientDisposable?.Dispose();
+                        transientDisposable = null;
+                    }
+                }
+                else if (Popup?.Host is OverlayPopupHost)
+                {
+                    // Same as above here, but just different coordinate space
+                    // so we don't need to translate
+                    if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false)
+                    {
+                        HideCore(false);
+                        enlargedPopupRect = null;
+                        transientDisposable?.Dispose();
+                        transientDisposable = null;
+                    }
+                }
+            }
+        }
+
+        protected virtual void OnOpening()
+        {
+            Opening?.Invoke(this, null);
+        }
+
+        protected virtual void OnOpened()
+        {
+            Opened?.Invoke(this, null);
+        }
+
+        protected virtual void OnClosing(CancelEventArgs args)
+        {
+            Closing?.Invoke(this, args);
+        }
+
+        protected virtual void OnClosed()
+        {
+            Closed?.Invoke(this, null);
+        }
+
+        /// <summary>
+        /// Used to create the content the Flyout displays
+        /// </summary>
+        /// <returns></returns>
+        protected abstract Control CreatePresenter();
+
+        private void InitPopup()
+        {
+            Popup = new Popup();
+            Popup.WindowManagerAddShadowHint = false;
+            Popup.IsLightDismissEnabled = true;
+
+            Popup.Opened += OnPopupOpened;
+            Popup.Closed += OnPopupClosed;
+        }
+
+        private void OnPopupOpened(object sender, EventArgs e)
+        {
+            IsOpen = true;
+        }
+
+        private void OnPopupClosed(object sender, EventArgs e)
+        {
+            HideCore();
+        }
+
+        private void PositionPopup(bool showAtPointer)
+        {
+            Size sz;
+            if(Popup.Child.DesiredSize == Size.Empty)
+            {
+                // Popup may not have been shown yet. Measure content
+                sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
+            }
+            else
+            {
+                sz = Popup.Child.DesiredSize;
+            }
+
+            if (showAtPointer)
+            {
+                Popup.PlacementMode = PlacementMode.Pointer;
+            }
+            else
+            {
+                Popup.PlacementMode = PlacementMode.AnchorAndGravity;
+                Popup.PlacementConstraintAdjustment =
+                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
+                    PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
+            }
+
+            var trgtBnds = Target?.Bounds ?? Rect.Empty;
+
+            switch (Placement)
+            {
+                case FlyoutPlacementMode.Top: //Above & centered
+                    Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
+                    break;
+
+                case FlyoutPlacementMode.TopEdgeAlignedLeft:
+                    Popup.PlacementRect = new Rect(0, 0, 0, 0);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;                    
+                    break;
+
+                case FlyoutPlacementMode.TopEdgeAlignedRight:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;                    
+                    break;
+
+                case FlyoutPlacementMode.RightEdgeAlignedTop:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
+                    break;
+
+                case FlyoutPlacementMode.Right: //Right & centered
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, trgtBnds.Height);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Right;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
+                    break;
+
+                case FlyoutPlacementMode.RightEdgeAlignedBottom:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
+                    break;
+
+                case FlyoutPlacementMode.Bottom: //Below & centered
+                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, trgtBnds.Width, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Bottom;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
+                    break;
+
+                case FlyoutPlacementMode.BottomEdgeAlignedLeft:
+                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
+                    break;
+
+                case FlyoutPlacementMode.BottomEdgeAlignedRight:
+                    Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
+                    break;
+
+                case FlyoutPlacementMode.LeftEdgeAlignedTop:
+                    Popup.PlacementRect = new Rect(0, 0, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
+                    break;
+
+                case FlyoutPlacementMode.Left: //Left & centered
+                    Popup.PlacementRect = new Rect(0, 0, 1, trgtBnds.Height);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Left;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
+                    break;
+
+                case FlyoutPlacementMode.LeftEdgeAlignedBottom:
+                    Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
+                    Popup.PlacementAnchor = PopupPositioning.PopupAnchor.BottomLeft;
+                    break;
+
+                //includes Auto (not sure what determines that)...
+                default:
+                    //This is just FlyoutPlacementMode.Top behavior (above & centered)
+                    Popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1);
+                    Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
+                    break;
+            }
+        }
+
+        private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
+        {
+            if (args.Sender is Control c)
+            {
+                if (args.OldValue is FlyoutBase)
+                {
+                    c.PointerReleased -= OnControlWithContextFlyoutPointerReleased;
+                }
+                if (args.NewValue is FlyoutBase)
+                {
+                    c.PointerReleased += OnControlWithContextFlyoutPointerReleased;
+                }
+            }
+        }
+
+        private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e)
+        {
+            if (sender is Control c)
+            {
+                if (e.InitialPressMouseButton == MouseButton.Right &&
+                e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased)
+                {
+                    if (c.ContextFlyout != null)
+                    {
+                        if (c.ContextMenu != null)
+                        {
+                            Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(c, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
+                            return;
+                        }
+                        c.ContextFlyout.ShowAt(c, true);
+                    }
+                }
+            }            
+        }
+
+        internal static void SetPresenterClasses(IControl presenter, Classes classes)
+        {
+            //Remove any classes no longer in use, ignoring pseudoclasses
+            for (int i = presenter.Classes.Count - 1; i >= 0; i--)
+            {
+                if (!classes.Contains(presenter.Classes[i]) &&
+                    !presenter.Classes[i].Contains(":"))
+                {
+                    presenter.Classes.RemoveAt(i);
+                }
+            }
+
+            //Add new classes
+            presenter.Classes.AddRange(classes);
+        }
+    }
+}

+ 77 - 0
src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs

@@ -0,0 +1,77 @@
+namespace Avalonia.Controls
+{
+    public enum FlyoutPlacementMode
+    {
+        /// <summary>
+        /// Preferred location is above the target element
+        /// </summary>
+        Top = 0,
+
+        /// <summary>
+        /// Preferred location is below the target element
+        /// </summary>
+        Bottom = 1,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element
+        /// </summary>
+        Left = 2,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element
+        /// </summary>
+        Right = 3,
+
+        //TODO
+        // <summary>
+        // Preferred location is centered on the screen
+        // </summary>
+        //Full = 4,
+
+        /// <summary>
+        /// Preferred location is above the target element, with the left edge of the flyout
+        /// aligned with the left edge of the target element
+        /// </summary>
+        TopEdgeAlignedLeft = 5,
+
+        /// <summary>
+        /// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element.
+        /// </summary>
+        TopEdgeAlignedRight = 6,
+
+        /// <summary>
+        /// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element.
+        /// </summary>
+        BottomEdgeAlignedLeft = 7,
+
+        /// <summary>
+        /// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element.
+        /// </summary>
+        BottomEdgeAlignedRight = 8,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element.
+        /// </summary>
+        LeftEdgeAlignedTop = 9,
+
+        /// <summary>
+        /// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
+        /// </summary>
+        LeftEdgeAlignedBottom = 10,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element.
+        /// </summary>
+        RightEdgeAlignedTop = 11,
+
+        /// <summary>
+        /// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
+        /// </summary>
+        RightEdgeAlignedBottom = 12,
+
+        /// <summary>
+        /// Preferred location is determined automatically.
+        /// </summary>
+        Auto = 13
+    }
+}

+ 33 - 0
src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs

@@ -0,0 +1,33 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls
+{
+    public class FlyoutPresenter : ContentControl
+    {
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
+
+        public CornerRadius CornerRadius
+        {
+            get => GetValue(CornerRadiusProperty);
+            set => SetValue(CornerRadiusProperty, value);
+        }
+
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            if (e.Key == Key.Escape)
+            {
+                var host = this.FindLogicalAncestorOfType<Popup>();
+                if (host != null)
+                {
+                    host.IsOpen = false;
+                    e.Handled = true;
+                }
+            }
+
+            base.OnKeyDown(e);            
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs

@@ -0,0 +1,24 @@
+namespace Avalonia.Controls
+{
+    // Note: FlyoutShowMode.Auto was removed. MS Docs just say:
+    // The show mode is determined automatically based on the method used to show the flyout.
+    // and AFAICT Flyouts generally open with "Standard" behavior
+
+    public enum FlyoutShowMode
+    {
+        /// <summary>
+        /// Behavior is typical of a flyout shown reactively, like a context menu. The open flyout takes focus. For a CommandBarFlyout, it opens in it's expanded state.
+        /// </summary>
+        Standard,
+
+        /// <summary>
+        /// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state.
+        /// </summary>
+        Transient,
+
+        /// <summary>
+        /// The flyout exhibits Transient behavior while the cursor is close to it, but is dismissed when the cursor moves away.
+        /// </summary>
+        TransientWithDismissOnPointerMoveAway
+    }
+}

+ 75 - 0
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@@ -0,0 +1,75 @@
+using System.Collections;
+using Avalonia.Collections;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Metadata;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+    public class MenuFlyout : FlyoutBase
+    {
+        public MenuFlyout()
+        {
+            _items = new AvaloniaList<object>();
+        }
+
+        /// <summary>
+        /// Defines the <see cref="Items"/> property
+        /// </summary>
+        public static readonly DirectProperty<MenuFlyout, IEnumerable> ItemsProperty =
+            ItemsControl.ItemsProperty.AddOwner<MenuFlyout>(x => x.Items,
+                (x, v) => x.Items = v);
+
+        /// <summary>
+        /// Defines the <see cref="ItemTemplate"/> property
+        /// </summary>
+        public static readonly DirectProperty<MenuFlyout, IDataTemplate?> ItemTemplateProperty =
+            AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate),
+                x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
+
+        public Classes FlyoutPresenterClasses => _classes ??= new Classes();
+
+        /// <summary>
+        /// Gets or sets the items of the MenuFlyout
+        /// </summary>
+        [Content]
+        public IEnumerable Items
+        {
+            get => _items;
+            set => SetAndRaise(ItemsProperty, ref _items, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the template used for the items
+        /// </summary>
+        public IDataTemplate? ItemTemplate
+        {
+            get => _itemTemplate;
+            set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
+        }
+
+        private Classes? _classes;
+        private IEnumerable _items;
+        private IDataTemplate? _itemTemplate;
+
+        protected override Control CreatePresenter()
+        {
+            return new MenuFlyoutPresenter
+            {
+                [!ItemsControl.ItemsProperty] = this[!ItemsProperty],
+                [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty]
+            };
+        }
+
+        protected override void OnOpened()
+        {
+            if (_classes != null)
+            {
+                SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
+            }
+            base.OnOpened();
+        }
+    }
+}

+ 55 - 0
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@@ -0,0 +1,55 @@
+using System;
+using Avalonia.Controls.Generators;
+using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives;
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls
+{
+    public class MenuFlyoutPresenter : MenuBase
+    {
+        public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
+            Border.CornerRadiusProperty.AddOwner<FlyoutPresenter>();
+
+        public CornerRadius CornerRadius
+        {
+            get => GetValue(CornerRadiusProperty);
+            set => SetValue(CornerRadiusProperty, value);
+        }
+
+        public MenuFlyoutPresenter()
+            :base(new DefaultMenuInteractionHandler(true))
+        {
+
+        }
+
+        public override void Close()
+        {
+            // DefaultMenuInteractionHandler calls this
+            var host = this.FindLogicalAncestorOfType<Popup>();
+            if (host != null)
+            {
+                for (int i = 0; i < LogicalChildren.Count; i++)
+                {
+                    if (LogicalChildren[i] is MenuItem item)
+                    {
+                        item.IsSubMenuOpen = false;
+                    }
+                }
+
+                SelectedIndex = -1;
+                host.IsOpen = false;                
+            }
+        }
+
+        public override void Open()
+        {
+            throw new NotSupportedException("Use MenuFlyout.ShowAt(Control) instead");
+        }
+
+        protected override IItemContainerGenerator CreateItemContainerGenerator()
+        {
+            return new MenuItemContainerGenerator(this);
+        }
+    }
+}

+ 2 - 1
src/Avalonia.Controls/TextBox.cs

@@ -492,7 +492,8 @@ namespace Avalonia.Controls
         {
             base.OnLostFocus(e);
 
-            if (ContextMenu == null || !ContextMenu.IsOpen)
+            if ((ContextFlyout == null || !ContextFlyout.IsOpen) &&
+                (ContextMenu == null || !ContextMenu.IsOpen))
             {
                 ClearSelection();
                 RevealPassword = false;

+ 2 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -60,4 +60,6 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.SplitView.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.DatePicker.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.TimePicker.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.FlyoutPresenter.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.MenuFlyoutPresenter.xaml?assembly=Avalonia.Themes.Default"/>
 </Styles>

+ 32 - 0
src/Avalonia.Themes.Default/FlyoutPresenter.xaml

@@ -0,0 +1,32 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="FlyoutPresenter">
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Stretch" />
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
+    <Setter Property="BorderThickness" Value="1" />
+    <Setter Property="Padding" Value="4" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ContentPresenter Content="{TemplateBinding Content}"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Margin="{TemplateBinding Padding}"
+                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
+                              HorizontalContentAlignment="Stretch"
+                              VerticalContentAlignment="Stretch" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 29 - 0
src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml

@@ -0,0 +1,29 @@
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="MenuFlyoutPresenter">
+    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
+    <Setter Property="BorderThickness" Value="1" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ItemsPresenter Name="PART_ItemsPresenter"
+                            Items="{TemplateBinding Items}"
+                            ItemsPanel="{TemplateBinding ItemsPanel}"
+                            ItemTemplate="{TemplateBinding ItemTemplate}"
+                            Margin="{TemplateBinding Padding}"
+                            KeyboardNavigation.TabNavigation="Continue"
+                            Grid.IsSharedSizeScope="True" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 9 - 1
src/Avalonia.Themes.Default/TextBox.xaml

@@ -3,6 +3,14 @@
     <StreamGeometry x:Key="TextBoxClearButtonData">M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
+
+    <MenuFlyout x:Key="DefaultTextBoxContextFlyout">
+      <MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
+      <MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
+      <MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
+    </MenuFlyout>
+
+    <!-- ContextMenu obsolete, prefer ContextFlyout --> 
     <ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
       <MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
       <MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
@@ -17,7 +25,7 @@
     <Setter Property="SelectionBrush" Value="{DynamicResource HighlightBrush}"/>
     <Setter Property="SelectionForegroundBrush" Value="{DynamicResource HighlightForegroundBrush}"/>
     <Setter Property="Padding" Value="4"/>
-    <Setter Property="ContextMenu" Value="{StaticResource DefaultTextBoxContextMenu}" />
+    <Setter Property="ContextFlyout" Value="{StaticResource DefaultTextBoxContextFlyout}" />
     <Setter Property="Template">
       <ControlTemplate>
         <Border Name="border"

+ 5 - 0
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml

@@ -809,5 +809,10 @@
     <StaticResource x:Key="CalendarDatePickerForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
     <StaticResource x:Key="CalendarDatePickerBorderBrush" ResourceKey="SystemControlForegroundBaseMediumBrush" />
     <Thickness x:Key="CalendarDatePickerBorderThemeThickness">1</Thickness>
+
+    <!-- Resources for FlyoutPresenter.xaml -->
+    <StaticResource x:Key="FlyoutPresenterBackground" ResourceKey="SystemControlBackgroundChromeMediumLowBrush" />
+    <StaticResource x:Key="FlyoutBorderThemeBrush" ResourceKey="SystemControlForegroundChromeHighBrush" />
+
   </Style.Resources>
 </Style>

+ 5 - 0
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml

@@ -807,5 +807,10 @@
     <StaticResource x:Key="CalendarDatePickerForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
     <StaticResource x:Key="CalendarDatePickerBorderBrush" ResourceKey="SystemControlForegroundBaseMediumBrush" />
     <Thickness x:Key="CalendarDatePickerBorderThemeThickness">1</Thickness>
+
+    <!-- Resources for FlyoutPresenter.xaml -->
+    <StaticResource x:Key="FlyoutPresenterBackground" ResourceKey="SystemControlBackgroundChromeMediumLowBrush" />
+    <StaticResource x:Key="FlyoutBorderThemeBrush" ResourceKey="SystemControlForegroundChromeHighBrush" />
+    
   </Style.Resources>
 </Style>

+ 2 - 0
src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml

@@ -59,4 +59,6 @@
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/SplitView.xaml"/>
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/DatePicker.xaml"/>  
   <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/TimePicker.xaml"/>  
+  <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml"/>
+  <StyleInclude Source="avares://Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml"/>
 </Styles>

+ 43 - 0
src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml

@@ -0,0 +1,43 @@
+<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  <Styles.Resources>
+    <Thickness x:Key="FlyoutBorderThemeThickness">1</Thickness>
+    <Thickness x:Key="FlyoutBorderThemePadding">0</Thickness>
+  </Styles.Resources>
+  
+  <Style Selector="FlyoutPresenter">
+    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
+    <Setter Property="VerticalContentAlignment" Value="Stretch" />
+    <Setter Property="Background" Value="{DynamicResource FlyoutPresenterBackground}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource FlyoutBorderThemeBrush}" />
+    <Setter Property="BorderThickness" Value="{DynamicResource FlyoutBorderThemeThickness}" />
+    <Setter Property="Padding" Value="{DynamicResource FlyoutContentThemePadding}" />
+    <Setter Property="MinWidth" Value="{DynamicResource FlyoutThemeMinWidth}" />
+    <Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" />
+    <Setter Property="MinHeight" Value="{DynamicResource FlyoutThemeMinHeight}" />
+    <Setter Property="MaxHeight" Value="{DynamicResource FlyoutThemeMaxHeight}" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Padding="{DynamicResource FlyoutBorderThemePadding}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ContentPresenter Content="{TemplateBinding Content}"
+                              ContentTemplate="{TemplateBinding ContentTemplate}"
+                              Margin="{TemplateBinding Padding}"
+                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
+                              HorizontalContentAlignment="Stretch"
+                              VerticalContentAlignment="Stretch" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 35 - 0
src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml

@@ -0,0 +1,35 @@
+<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+  
+  <Style Selector="MenuFlyoutPresenter">
+    <Setter Property="Background" Value="{DynamicResource MenuFlyoutPresenterBackground}" />
+    <Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderBrush}" />
+    <Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" />
+    <Setter Property="Padding" Value="{DynamicResource MenuFlyoutPresenterThemePadding}" />
+    <Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" />
+    <Setter Property="MinHeight" Value="{DynamicResource MenuFlyoutThemeMinHeight}" />
+    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
+    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
+    <Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" />
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Name="LayoutRoot"
+                Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                Padding="{DynamicResource FlyoutBorderThemePadding}"
+                CornerRadius="{TemplateBinding CornerRadius}">
+          <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
+                        VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
+            <ItemsPresenter Name="PART_ItemsPresenter"
+                            Items="{TemplateBinding Items}"
+                            ItemsPanel="{TemplateBinding ItemsPanel}"
+                            ItemTemplate="{TemplateBinding ItemTemplate}"
+                            Margin="{DynamicResource MenuFlyoutScrollerMargin}"
+                            KeyboardNavigation.TabNavigation="Continue"
+                            Grid.IsSharedSizeScope="True" />
+          </ScrollViewer>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+</Styles>

+ 9 - 1
src/Avalonia.Themes.Fluent/Controls/TextBox.xaml

@@ -14,6 +14,14 @@
     <StreamGeometry x:Key="TextBoxClearButtonData">M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxRevealButtonData">m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z</StreamGeometry>
     <StreamGeometry x:Key="PasswordBoxHideButtonData">m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z</StreamGeometry>
+
+    <MenuFlyout x:Key="DefaultTextBoxContextFlyout">
+      <MenuItem x:Name="TextBoxContextFlyoutCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
+      <MenuItem x:Name="TextBoxContextFlyoutCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
+      <MenuItem x:Name="TextBoxContextFlyoutPasteItem" Header="Paste" Command="{Binding $parent[TextBox].Paste}" IsEnabled="{Binding $parent[TextBox].CanPaste}" InputGesture="{x:Static TextBox.PasteGesture}"/>
+    </MenuFlyout>
+
+    <!-- ContextMenu obsolete, prefer ContextFlyout -->
     <ContextMenu x:Key="DefaultTextBoxContextMenu" x:Name="TextBoxContextMenu">
       <MenuItem x:Name="TextBoxContextMenuCutItem" Header="Cut" Command="{Binding $parent[TextBox].Cut}" IsEnabled="{Binding $parent[TextBox].CanCut}" InputGesture="{x:Static TextBox.CutGesture}" />
       <MenuItem x:Name="TextBoxContextMenuCopyItem" Header="Copy" Command="{Binding $parent[TextBox].Copy}" IsEnabled="{Binding $parent[TextBox].CanCopy}" InputGesture="{x:Static TextBox.CopyGesture}"/>
@@ -33,7 +41,7 @@
     <Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
     <Setter Property="Padding" Value="{DynamicResource TextControlThemePadding}" />
     <Setter Property="FocusAdorner" Value="{x:Null}" />
-    <Setter Property="ContextMenu" Value="{StaticResource DefaultTextBoxContextMenu}" />
+    <Setter Property="ContextFlyout" Value="{StaticResource DefaultTextBoxContextFlyout}" />
     <Setter Property="Template">
       <ControlTemplate>
         <DataValidationErrors>

+ 325 - 0
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@@ -0,0 +1,325 @@
+using System;
+using System.Linq;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class FlyoutTests
+    {
+        [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);
+            }
+        }
+
+        [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 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();
+
+                int tracker = 0;
+                Flyout f = new Flyout();
+                f.Closing += (s, e) =>
+                {
+                    e.Cancel = true;
+                };
+                f.ShowAt(window);
+                f.Hide();
+
+                Assert.True(f.IsOpen);
+            }
+        }
+
+        [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.True(FocusManager.Instance?.Current == button);
+                button.Flyout.ShowAt(button);
+                Assert.False(button.IsFocused);
+                Assert.True(FocusManager.Instance?.Current == flyoutTextBox);
+            }
+        }
+
+        [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();
+
+                FocusManager.Instance?.Focus(button);
+                Assert.True(FocusManager.Instance?.Current == button);
+                button.Flyout.ShowAt(button);
+                Assert.True(FocusManager.Instance?.Current == button);
+            }
+        }
+
+        [Fact]
+        public void ContextFlyout_Can_Be_Set_In_Styles()
+        {
+            using (CreateServicesWithFocus())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='TextBlock'>
+            <Setter Property='ContextFlyout'>
+                <MenuFlyout>
+                    <MenuItem>Foo</MenuItem>
+                </MenuFlyout>
+            </Setter>
+        </Style>
+	</Window.Styles>
+
+    <StackPanel>
+        <TextBlock Name='target1'/>
+        <TextBlock Name='target2'/>
+    </StackPanel>
+</Window>";
+
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var target1 = window.Find<TextBlock>("target1");
+                var target2 = window.Find<TextBlock>("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 = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='FlyoutPresenter.TestClass'>
+            <Setter Property='Background' Value='Red' />
+        </Style>
+	</Window.Styles>
+</Window>";
+
+                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<FlyoutPresenter>().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 =>
+                    {
+                        return MockWindowingPlatform.CreatePopupMock(x).Object;
+                    }),
+                    focusManager: new FocusManager(),
+                    keyboardDevice: () => new KeyboardDevice()));
+        }
+
+        private Window PreparedWindow(object content = null)
+        {
+            var w = new Window { Content = content };
+            w.ApplyTemplate();
+            return w;
+        }
+    }
+}

+ 39 - 1
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls.Presenters;
@@ -56,7 +57,44 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal("123", target1.SelectedText);
             }
         }
-        
+
+        [Fact]
+        public void Opening_Context_Flyout_Does_not_Lose_Selection()
+        {
+            using (UnitTestApplication.Start(FocusServices))
+            {
+                var target1 = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "1234",
+                    ContextFlyout = new MenuFlyout
+                    {
+                        Items = new List<MenuItem>
+                        {
+                            new MenuItem { Header = "Item 1" },
+                            new MenuItem {Header = "Item 2" },
+                            new MenuItem {Header = "Item 3" }
+                        }
+                    }
+                };
+                              
+
+                target1.ApplyTemplate();
+
+                var root = new TestRoot() { Child = target1 };
+
+                target1.SelectionStart = 0;
+                target1.SelectionEnd = 3;
+
+                target1.Focus();
+                Assert.True(target1.IsFocused);
+
+                target1.ContextFlyout.ShowAt(target1);
+
+                Assert.Equal("123", target1.SelectedText);
+            }
+        }
+
         [Fact]
         public void DefaultBindingMode_Should_Be_TwoWay()
         {