mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
604 lines
20 KiB
C#
604 lines
20 KiB
C#
using System.Reflection;
|
|
using AtomUI.Controls;
|
|
using AtomUI.Controls.Primitives;
|
|
using Avalonia;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Threading;
|
|
using Avalonia.VisualTree;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.TabControl;
|
|
|
|
public class TabOverflowMenuTemplateTests
|
|
{
|
|
static TabOverflowMenuTemplateTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabStrip_Overflow_Items_Preserve_Header_ContentTemplate()
|
|
{
|
|
var template = CreateHeaderTemplate();
|
|
var tabStrip = new CardTabStrip
|
|
{
|
|
Width = 220,
|
|
ItemsSource = CreateTabs(),
|
|
ItemTemplate = template,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
|
|
ShowInWindow(tabStrip, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabStrip, window);
|
|
var overflowItems = presenter.MenuFlyout!.Items
|
|
.OfType<TabStripOverflowMenuItem>()
|
|
.ToList();
|
|
|
|
overflowItems.ShouldNotBeEmpty();
|
|
foreach (var overflowItem in overflowItems)
|
|
{
|
|
overflowItem.TabStripItem.ShouldNotBeNull();
|
|
overflowItem.Header.ShouldBeSameAs(overflowItem.TabStripItem.Content);
|
|
overflowItem.HeaderTemplate.ShouldBeSameAs(overflowItem.TabStripItem.ContentTemplate);
|
|
overflowItem.HeaderTemplate.ShouldBeSameAs(template);
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabControl_Overflow_Items_Preserve_HeaderTemplate()
|
|
{
|
|
var template = CreateHeaderTemplate();
|
|
var tabControl = new CardTabControl
|
|
{
|
|
Width = 220,
|
|
Height = 180,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var tab in CreateTabs())
|
|
{
|
|
tabControl.Items.Add(new TabItem
|
|
{
|
|
Header = tab,
|
|
HeaderTemplate = template,
|
|
Content = tab.Title
|
|
});
|
|
}
|
|
|
|
ShowInWindow(tabControl, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabControl, window);
|
|
var overflowItems = presenter.MenuFlyout!.Items
|
|
.OfType<TabControlOverflowMenuItem>()
|
|
.ToList();
|
|
|
|
overflowItems.ShouldNotBeEmpty();
|
|
foreach (var overflowItem in overflowItems)
|
|
{
|
|
overflowItem.TabItem.ShouldNotBeNull();
|
|
overflowItem.Header.ShouldBeSameAs(overflowItem.TabItem.Header);
|
|
overflowItem.HeaderTemplate.ShouldBeSameAs(overflowItem.TabItem.HeaderTemplate);
|
|
overflowItem.HeaderTemplate.ShouldBeSameAs(template);
|
|
}
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabControl_Overflow_Item_Hides_Close_Button_When_Tab_Is_Not_Closable()
|
|
{
|
|
var tabControl = new CardTabControl
|
|
{
|
|
Width = 220,
|
|
Height = 180,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = false,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var tab in CreateTabItems())
|
|
{
|
|
tabControl.Items.Add(tab);
|
|
}
|
|
|
|
ShowInWindow(tabControl, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabControl, window);
|
|
var overflowItem = presenter.MenuFlyout!.Items
|
|
.OfType<TabControlOverflowMenuItem>()
|
|
.First();
|
|
|
|
overflowItem.IsClosable.ShouldBeFalse();
|
|
GetOverflowCloseButton(overflowItem).IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabStrip_Overflow_Item_Hides_Close_Button_When_Tab_Is_Not_Closable()
|
|
{
|
|
var tabStrip = new CardTabStrip
|
|
{
|
|
Width = 220,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = false,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var tab in CreateTabStripItems())
|
|
{
|
|
tabStrip.Items.Add(tab);
|
|
}
|
|
|
|
ShowInWindow(tabStrip, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabStrip, window);
|
|
var overflowItem = presenter.MenuFlyout!.Items
|
|
.OfType<TabStripOverflowMenuItem>()
|
|
.First();
|
|
|
|
overflowItem.IsClosable.ShouldBeFalse();
|
|
GetOverflowCloseButton(overflowItem).IsVisible.ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabControl_Overflow_Close_Uses_Owner_Closing_Contract()
|
|
{
|
|
var tabControl = new CardTabControl
|
|
{
|
|
Width = 220,
|
|
Height = 180,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var tab in CreateTabItems())
|
|
{
|
|
tabControl.Items.Add(tab);
|
|
}
|
|
|
|
var closingCount = 0;
|
|
var closedCount = 0;
|
|
tabControl.Closing += (_, args) =>
|
|
{
|
|
closingCount++;
|
|
args.Cancel = true;
|
|
};
|
|
tabControl.Closed += (_, _) => closedCount++;
|
|
|
|
ShowInWindow(tabControl, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabControl, window);
|
|
var overflowItem = presenter.MenuFlyout!.Items
|
|
.OfType<TabControlOverflowMenuItem>()
|
|
.First();
|
|
var itemCount = tabControl.Items.Count;
|
|
var menuCount = presenter.MenuFlyout.Items.Count;
|
|
|
|
ClickOverflowCloseButton(overflowItem);
|
|
|
|
closingCount.ShouldBe(1);
|
|
closedCount.ShouldBe(0);
|
|
tabControl.Items.Count.ShouldBe(itemCount);
|
|
presenter.MenuFlyout.Items.Count.ShouldBe(menuCount);
|
|
presenter.MenuFlyout.Items.ShouldContain(overflowItem);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabStrip_Overflow_Close_Uses_Owner_Closing_Contract()
|
|
{
|
|
var tabStrip = new CardTabStrip
|
|
{
|
|
Width = 220,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var tab in CreateTabStripItems())
|
|
{
|
|
tabStrip.Items.Add(tab);
|
|
}
|
|
|
|
var closingCount = 0;
|
|
var closedCount = 0;
|
|
tabStrip.Closing += (_, args) =>
|
|
{
|
|
closingCount++;
|
|
args.Cancel = true;
|
|
};
|
|
tabStrip.Closed += (_, _) => closedCount++;
|
|
|
|
ShowInWindow(tabStrip, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabStrip, window);
|
|
var overflowItem = presenter.MenuFlyout!.Items
|
|
.OfType<TabStripOverflowMenuItem>()
|
|
.First();
|
|
var itemCount = tabStrip.Items.Count;
|
|
var menuCount = presenter.MenuFlyout.Items.Count;
|
|
|
|
ClickOverflowCloseButton(overflowItem);
|
|
|
|
closingCount.ShouldBe(1);
|
|
closedCount.ShouldBe(0);
|
|
tabStrip.Items.Count.ShouldBe(itemCount);
|
|
presenter.MenuFlyout.Items.Count.ShouldBe(menuCount);
|
|
presenter.MenuFlyout.Items.ShouldContain(overflowItem);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabControl_Overflow_Close_Removes_Menu_Item_After_Owner_Succeeds()
|
|
{
|
|
var tabControl = new CardTabControl
|
|
{
|
|
Width = 220,
|
|
Height = 180,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var tab in CreateTabItems())
|
|
{
|
|
tabControl.Items.Add(tab);
|
|
}
|
|
|
|
var eventOrder = new List<string>();
|
|
tabControl.Closing += (_, _) => eventOrder.Add("closing");
|
|
tabControl.Closed += (_, _) => eventOrder.Add("closed");
|
|
|
|
ShowInWindow(tabControl, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabControl, window);
|
|
var overflowItem = presenter.MenuFlyout!.Items
|
|
.OfType<TabControlOverflowMenuItem>()
|
|
.First();
|
|
var tabItem = overflowItem.TabItem.ShouldNotBeNull();
|
|
var itemCount = tabControl.Items.Count;
|
|
|
|
ClickOverflowCloseButton(overflowItem);
|
|
|
|
eventOrder.ShouldBe(["closing", "closed"]);
|
|
tabControl.Items.Count.ShouldBe(itemCount - 1);
|
|
tabControl.Items.ShouldNotContain(tabItem);
|
|
presenter.MenuFlyout.Items.ShouldNotContain(overflowItem);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabStrip_Overflow_Close_Removes_Menu_Item_After_Owner_Succeeds()
|
|
{
|
|
var tabStrip = new CardTabStrip
|
|
{
|
|
Width = 220,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
foreach (var tab in CreateTabStripItems())
|
|
{
|
|
tabStrip.Items.Add(tab);
|
|
}
|
|
|
|
var eventOrder = new List<string>();
|
|
tabStrip.Closing += (_, _) => eventOrder.Add("closing");
|
|
tabStrip.Closed += (_, _) => eventOrder.Add("closed");
|
|
|
|
ShowInWindow(tabStrip, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabStrip, window);
|
|
var overflowItem = presenter.MenuFlyout!.Items
|
|
.OfType<TabStripOverflowMenuItem>()
|
|
.First();
|
|
var tabItem = overflowItem.TabStripItem.ShouldNotBeNull();
|
|
var itemCount = tabStrip.Items.Count;
|
|
|
|
ClickOverflowCloseButton(overflowItem);
|
|
|
|
eventOrder.ShouldBe(["closing", "closed"]);
|
|
tabStrip.Items.Count.ShouldBe(itemCount - 1);
|
|
tabStrip.Items.ShouldNotContain(tabItem);
|
|
presenter.MenuFlyout.Items.ShouldNotContain(overflowItem);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabStrip_Overflow_Close_Removes_Item_From_Writable_ItemsSource()
|
|
{
|
|
var source = CreateTabs().ToList();
|
|
var tabStrip = new CardTabStrip
|
|
{
|
|
Width = 220,
|
|
ItemsSource = source,
|
|
ItemTemplate = CreateHeaderTemplate(),
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false
|
|
};
|
|
|
|
var closedCount = 0;
|
|
tabStrip.Closed += (_, _) => closedCount++;
|
|
|
|
ShowInWindow(tabStrip, window =>
|
|
{
|
|
var presenter = OpenOverflowMenu(tabStrip, window);
|
|
var overflowItem = presenter.MenuFlyout!.Items
|
|
.OfType<TabStripOverflowMenuItem>()
|
|
.First();
|
|
var tabStripItem = overflowItem.TabStripItem.ShouldNotBeNull();
|
|
var sourceItem = tabStripItem.Content.ShouldBeOfType<DemoTab>();
|
|
|
|
source.ShouldContain(sourceItem);
|
|
ClickOverflowCloseButton(overflowItem);
|
|
|
|
closedCount.ShouldBe(1);
|
|
source.ShouldNotContain(sourceItem);
|
|
tabStrip.Items.Count.ShouldBe(source.Count);
|
|
presenter.MenuFlyout.Items.ShouldNotContain(overflowItem);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabControl_Pinned_Overflow_Menu_Reopens_After_Detach()
|
|
{
|
|
var tabControl = new CardTabControl
|
|
{
|
|
Width = 220,
|
|
Height = 180,
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false,
|
|
IsPopupPinnedOpen = true
|
|
};
|
|
foreach (var tab in CreateTabs())
|
|
{
|
|
tabControl.Items.Add(new TabItem
|
|
{
|
|
Header = tab.Title,
|
|
Content = tab.Title
|
|
});
|
|
}
|
|
|
|
ShowInWindowWithHost(tabControl, (_, host) =>
|
|
{
|
|
var scrollViewer = GetOverflowScrollViewer(tabControl);
|
|
var flyout = GetMenuFlyout(scrollViewer);
|
|
flyout.IsOpen.ShouldBeTrue();
|
|
flyout.Popup.IsOpen.ShouldBeTrue();
|
|
|
|
flyout.Hide();
|
|
Dispatcher.UIThread.RunJobs();
|
|
flyout.IsOpen.ShouldBeTrue();
|
|
flyout.Popup.IsOpen.ShouldBeTrue();
|
|
|
|
host.Children.Remove(tabControl);
|
|
Dispatcher.UIThread.RunJobs();
|
|
flyout.IsOpen.ShouldBeFalse();
|
|
flyout.Popup.IsOpen.ShouldBeFalse();
|
|
flyout.IsPopupPinnedOpen.ShouldBeFalse();
|
|
|
|
host.Children.Add(tabControl);
|
|
Dispatcher.UIThread.RunJobs();
|
|
var reopenedFlyout = GetMenuFlyout(GetOverflowScrollViewer(tabControl));
|
|
reopenedFlyout.IsOpen.ShouldBeTrue();
|
|
reopenedFlyout.Popup.IsOpen.ShouldBeTrue();
|
|
|
|
tabControl.IsPopupPinnedOpen = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
reopenedFlyout.IsOpen.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void CardTabStrip_Pinned_Overflow_Menu_Reopens_After_Detach()
|
|
{
|
|
var tabStrip = new CardTabStrip
|
|
{
|
|
Width = 220,
|
|
ItemsSource = CreateTabs(),
|
|
ItemTemplate = CreateHeaderTemplate(),
|
|
SelectedIndex = 0,
|
|
IsTabClosable = true,
|
|
IsMotionEnabled = false,
|
|
IsPopupPinnedOpen = true
|
|
};
|
|
|
|
ShowInWindowWithHost(tabStrip, (_, host) =>
|
|
{
|
|
var scrollViewer = GetOverflowScrollViewer(tabStrip);
|
|
var flyout = GetMenuFlyout(scrollViewer);
|
|
flyout.IsOpen.ShouldBeTrue();
|
|
flyout.Popup.IsOpen.ShouldBeTrue();
|
|
|
|
flyout.Hide();
|
|
Dispatcher.UIThread.RunJobs();
|
|
flyout.IsOpen.ShouldBeTrue();
|
|
flyout.Popup.IsOpen.ShouldBeTrue();
|
|
|
|
host.Children.Remove(tabStrip);
|
|
Dispatcher.UIThread.RunJobs();
|
|
flyout.IsOpen.ShouldBeFalse();
|
|
flyout.Popup.IsOpen.ShouldBeFalse();
|
|
flyout.IsPopupPinnedOpen.ShouldBeFalse();
|
|
|
|
host.Children.Add(tabStrip);
|
|
Dispatcher.UIThread.RunJobs();
|
|
var reopenedFlyout = GetMenuFlyout(GetOverflowScrollViewer(tabStrip));
|
|
reopenedFlyout.IsOpen.ShouldBeTrue();
|
|
reopenedFlyout.Popup.IsOpen.ShouldBeTrue();
|
|
|
|
tabStrip.IsPopupPinnedOpen = false;
|
|
Dispatcher.UIThread.RunJobs();
|
|
reopenedFlyout.IsOpen.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
private static MenuFlyoutPresenter OpenOverflowMenu(Control owner, AvaloniaWindow window)
|
|
{
|
|
var scrollViewer = GetOverflowScrollViewer(owner);
|
|
scrollViewer.Extent.Width.ShouldBeGreaterThan(scrollViewer.Viewport.Width);
|
|
|
|
var menuIndicator = scrollViewer.GetVisualDescendants()
|
|
.OfType<IconButton>()
|
|
.Single(button => button.Name == "PART_ScrollMenuIndicator");
|
|
menuIndicator.IsVisible.ShouldBeTrue();
|
|
menuIndicator.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, menuIndicator));
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
return window.GetVisualDescendants()
|
|
.OfType<MenuFlyoutPresenter>()
|
|
.ShouldHaveSingleItem();
|
|
}
|
|
|
|
private static BaseTabScrollViewer GetOverflowScrollViewer(Control owner)
|
|
{
|
|
var scrollViewer = owner.GetVisualDescendants()
|
|
.OfType<BaseTabScrollViewer>()
|
|
.ShouldHaveSingleItem();
|
|
scrollViewer.Extent.Width.ShouldBeGreaterThan(scrollViewer.Viewport.Width);
|
|
return scrollViewer;
|
|
}
|
|
|
|
private static MenuFlyout GetMenuFlyout(BaseTabScrollViewer scrollViewer)
|
|
{
|
|
var field = typeof(BaseTabScrollViewer).GetField(
|
|
"MenuFlyout",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
field.ShouldNotBeNull();
|
|
return field.GetValue(scrollViewer).ShouldBeOfType<MenuFlyout>();
|
|
}
|
|
|
|
private static IconButton GetOverflowCloseButton(BaseOverflowMenuItem overflowItem)
|
|
{
|
|
return overflowItem.GetVisualDescendants()
|
|
.OfType<IconButton>()
|
|
.Single(button => button.Name == "PART_ItemCloseButton");
|
|
}
|
|
|
|
private static void ClickOverflowCloseButton(BaseOverflowMenuItem overflowItem)
|
|
{
|
|
var closeButton = GetOverflowCloseButton(overflowItem);
|
|
closeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, closeButton));
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
|
|
private static FuncDataTemplate<DemoTab> CreateHeaderTemplate()
|
|
{
|
|
return new FuncDataTemplate<DemoTab>(
|
|
(item, _) => new TextBlock { Text = $"Rendered:{item?.Title}" });
|
|
}
|
|
|
|
private static DemoTab[] CreateTabs()
|
|
{
|
|
return Enumerable.Range(1, 8)
|
|
.Select(index => new DemoTab($"Document {index:00}"))
|
|
.ToArray();
|
|
}
|
|
|
|
private static TabItem[] CreateTabItems()
|
|
{
|
|
return Enumerable.Range(1, 8)
|
|
.Select(index => new TabItem
|
|
{
|
|
Header = $"Document {index:00}",
|
|
Content = $"Content {index:00}"
|
|
})
|
|
.ToArray();
|
|
}
|
|
|
|
private static TabStripItem[] CreateTabStripItems()
|
|
{
|
|
return Enumerable.Range(1, 8)
|
|
.Select(index => new TabStripItem
|
|
{
|
|
Content = $"Document {index:00}"
|
|
})
|
|
.ToArray();
|
|
}
|
|
|
|
private static void ShowInWindow(Control content, Action<AvaloniaWindow> assertion)
|
|
{
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 260,
|
|
Content = CreatePopupOverlayHost(content)
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
assertion(window);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static void ShowInWindowWithHost(
|
|
Control content,
|
|
Action<AvaloniaWindow, ScopeAwareOverlayLayerPanel> assertion)
|
|
{
|
|
var overlayPanel = new ScopeAwareOverlayLayerPanel
|
|
{
|
|
Width = 360,
|
|
Height = 260
|
|
};
|
|
overlayPanel.Children.Add(content);
|
|
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 260,
|
|
Content = CreatePopupOverlayHost(overlayPanel)
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
assertion(window, overlayPanel);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static VisualLayerManager CreatePopupOverlayHost(Control content)
|
|
{
|
|
var overlayPanel = new ScopeAwareOverlayLayerPanel
|
|
{
|
|
Width = 360,
|
|
Height = 260
|
|
};
|
|
overlayPanel.Children.Add(content);
|
|
|
|
var visualLayerManager = new VisualLayerManager
|
|
{
|
|
Child = overlayPanel
|
|
};
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
return visualLayerManager;
|
|
}
|
|
|
|
private sealed class DemoTab(string title)
|
|
{
|
|
public string Title { get; } = title;
|
|
}
|
|
}
|