mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-18 14:37:19 +08:00
883 lines
30 KiB
C#
883 lines
30 KiB
C#
using Avalonia.Controls;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Headless;
|
|
using Avalonia.Input;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using AvaloniaWindow = Avalonia.Controls.Window;
|
|
|
|
namespace AtomUI.Desktop.Controls.Tests.NavMenu;
|
|
|
|
public class NavMenuKeyboardNavigationTests
|
|
{
|
|
static NavMenuKeyboardNavigationTests()
|
|
{
|
|
AvaloniaTestApp.EnsureInitialized();
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Arrow_Keys_Move_Keyboard_Active_Item_Without_Selecting_Until_Enter()
|
|
{
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var second = new NavMenuNode
|
|
{
|
|
Header = "Second",
|
|
ItemKey = "second"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
|
|
var selectedNodes = new List<INavMenuNode>();
|
|
var clickedItems = new List<INavMenuItem>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
menu.NavMenuItemClick += (_, args) => clickedItems.Add(args.NavMenuItem);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var firstContainer = (NavMenuItem)menu.ContainerFromItem(first)!;
|
|
var secondContainer = (NavMenuItem)menu.ContainerFromItem(second)!;
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue(
|
|
"NavMenu itself must be focusable so keyboard navigation can start before any item is selected.");
|
|
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(firstContainer).ShouldBeTrue();
|
|
IsActive(secondContainer).ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeNull();
|
|
selectedNodes.ShouldBeEmpty();
|
|
clickedItems.ShouldBeEmpty();
|
|
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(firstContainer).ShouldBeFalse();
|
|
IsActive(secondContainer).ShouldBeTrue();
|
|
menu.SelectedItem.ShouldBeNull();
|
|
selectedNodes.ShouldBeEmpty();
|
|
clickedItems.ShouldBeEmpty();
|
|
|
|
PressKey(window, Key.Enter, PhysicalKey.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeSameAs(second);
|
|
selectedNodes.ShouldBe([second]);
|
|
clickedItems.Count.ShouldBe(1);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Removing_The_Keyboard_Active_Item_Invalidates_It_Before_Enter()
|
|
{
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var second = new NavMenuNode
|
|
{
|
|
Header = "Second",
|
|
ItemKey = "second"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
|
|
var selectedNodes = new List<INavMenuNode>();
|
|
var clickedItems = new List<INavMenuItem>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
menu.NavMenuItemClick += (_, args) => clickedItems.Add(args.NavMenuItem);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var firstContainer = menu.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
|
|
var secondContainer = menu.ContainerFromItem(second).ShouldBeOfType<NavMenuItem>();
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
IsActive(firstContainer).ShouldBeTrue();
|
|
|
|
menu.Items.Remove(first);
|
|
Dispatcher.UIThread.RunJobs();
|
|
window.UpdateLayout();
|
|
|
|
PressKey(window, Key.Enter, PhysicalKey.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeNull();
|
|
selectedNodes.ShouldBeEmpty();
|
|
clickedItems.ShouldBeEmpty();
|
|
IsActive(firstContainer).ShouldBeFalse();
|
|
IsActive(secondContainer).ShouldBeTrue(
|
|
"After the recycled active container is forgotten, Enter should only establish a new active target.");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Keyboard_Navigation_Traverses_Groups_And_Skips_Dividers_In_Entry_Order()
|
|
{
|
|
var first = new NavMenuNode { Header = "First", ItemKey = "first" };
|
|
var second = new NavMenuNode { Header = "Second", ItemKey = "second" };
|
|
var third = new NavMenuNode { Header = "Third", ItemKey = "third" };
|
|
var fourth = new NavMenuNode { Header = "Fourth", ItemKey = "fourth" };
|
|
var nestedGroup = new NavMenuGroup { Header = "Nested" };
|
|
nestedGroup.Entries.Add(third);
|
|
var group = new NavMenuGroup { Header = "Section" };
|
|
group.Entries.Add(second);
|
|
group.Entries.Add(new NavMenuDivider());
|
|
group.Entries.Add(nestedGroup);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(new NavMenuDivider());
|
|
menu.Items.Add(group);
|
|
menu.Items.Add(fourth);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var firstContainer = menu.ContainerFromItem(first).ShouldBeOfType<NavMenuItem>();
|
|
var groupContainer = menu.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
|
|
var secondContainer = groupContainer.ContainerFromItem(second).ShouldBeOfType<NavMenuItem>();
|
|
var nestedGroupContainer = groupContainer.ContainerFromItem(nestedGroup).ShouldBeOfType<NavMenuGroupItem>();
|
|
var thirdContainer = nestedGroupContainer.ContainerFromItem(third).ShouldBeOfType<NavMenuItem>();
|
|
var fourthContainer = menu.ContainerFromItem(fourth).ShouldBeOfType<NavMenuItem>();
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
IsActive(firstContainer).ShouldBeTrue();
|
|
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
IsActive(secondContainer).ShouldBeTrue();
|
|
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
IsActive(thirdContainer).ShouldBeTrue();
|
|
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
IsActive(fourthContainer).ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Vertical_Keyboard_Enters_A_Grouped_Child_And_Left_Returns_To_Its_Semantic_Parent()
|
|
{
|
|
var child = new NavMenuNode { Header = "Child", ItemKey = "child" };
|
|
var group = new NavMenuGroup { Header = "Section" };
|
|
group.Entries.Add(child);
|
|
var parent = new NavMenuNode { Header = "Parent", ItemKey = "parent" };
|
|
parent.Entries.Add(group);
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
ShowInWindow(CreatePopupOverlayHost(menu), window =>
|
|
{
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
IsActive(parentContainer).ShouldBeTrue();
|
|
|
|
PressKey(window, Key.Right, PhysicalKey.ArrowRight);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var groupContainer = parentContainer.ContainerFromItem(group).ShouldBeOfType<NavMenuGroupItem>();
|
|
var childContainer = groupContainer.ContainerFromItem(child).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
IsActive(childContainer).ShouldBeTrue();
|
|
|
|
PressKey(window, Key.Left, PhysicalKey.ArrowLeft);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
parentContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
IsActive(parentContainer).ShouldBeTrue();
|
|
IsActive(childContainer).ShouldBeFalse();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Initial_Keyboard_Active_Starts_From_First_Item_When_No_Item_Is_Selected()
|
|
{
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var second = new NavMenuNode
|
|
{
|
|
Header = "Second",
|
|
ItemKey = "second"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var firstContainer = (NavMenuItem)menu.ContainerFromItem(first)!;
|
|
var secondContainer = (NavMenuItem)menu.ContainerFromItem(second)!;
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Up, PhysicalKey.ArrowUp);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(firstContainer).ShouldBeTrue(
|
|
"When no item is selected, the first keyboard movement should initialize active state from the first visible item, independent of direction.");
|
|
IsActive(secondContainer).ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_First_Down_Key_Moves_From_Selected_Item_To_Next_Item()
|
|
{
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var second = new NavMenuNode
|
|
{
|
|
Header = "Second",
|
|
ItemKey = "second"
|
|
};
|
|
var third = new NavMenuNode
|
|
{
|
|
Header = "Third",
|
|
ItemKey = "third"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
menu.Items.Add(third);
|
|
|
|
var selectedNodes = new List<INavMenuNode>();
|
|
var clickedItems = new List<INavMenuItem>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
menu.NavMenuItemClick += (_, args) => clickedItems.Add(args.NavMenuItem);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var firstContainer = (NavMenuItem)menu.ContainerFromItem(first)!;
|
|
var secondContainer = (NavMenuItem)menu.ContainerFromItem(second)!;
|
|
var thirdContainer = (NavMenuItem)menu.ContainerFromItem(third)!;
|
|
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(secondContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeSameAs(second);
|
|
selectedNodes.Clear();
|
|
clickedItems.Clear();
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(firstContainer).ShouldBeFalse();
|
|
IsActive(secondContainer).ShouldBeFalse(
|
|
"The selected item is only the initial keyboard navigation anchor; the first Down key must still move to the next item.");
|
|
IsActive(thirdContainer).ShouldBeTrue();
|
|
menu.SelectedItem.ShouldBeSameAs(second);
|
|
selectedNodes.ShouldBeEmpty();
|
|
clickedItems.ShouldBeEmpty();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_First_Up_Key_Moves_From_Selected_Item_To_Previous_Item()
|
|
{
|
|
var first = new NavMenuNode
|
|
{
|
|
Header = "First",
|
|
ItemKey = "first"
|
|
};
|
|
var second = new NavMenuNode
|
|
{
|
|
Header = "Second",
|
|
ItemKey = "second"
|
|
};
|
|
var third = new NavMenuNode
|
|
{
|
|
Header = "Third",
|
|
ItemKey = "third"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(first);
|
|
menu.Items.Add(second);
|
|
menu.Items.Add(third);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var firstContainer = (NavMenuItem)menu.ContainerFromItem(first)!;
|
|
var secondContainer = (NavMenuItem)menu.ContainerFromItem(second)!;
|
|
var thirdContainer = (NavMenuItem)menu.ContainerFromItem(third)!;
|
|
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(secondContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Up, PhysicalKey.ArrowUp);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(firstContainer).ShouldBeTrue();
|
|
IsActive(secondContainer).ShouldBeFalse(
|
|
"The selected item is only the initial keyboard navigation anchor; the first Up key must still move to the previous item.");
|
|
IsActive(thirdContainer).ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeSameAs(second);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Left_Key_Collapses_Active_Submenu_Without_Moving_Keyboard_Active_Item()
|
|
{
|
|
var option = new NavMenuNode
|
|
{
|
|
Header = "Option",
|
|
ItemKey = "option"
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
child.Children.Add(option);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var childContainer = (NavMenuItem)parentContainer.ContainerFromItem(child)!;
|
|
childContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var optionContainer = (NavMenuItem)childContainer.ContainerFromItem(option)!;
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(childContainer).ShouldBeTrue();
|
|
childContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
PressKey(window, Key.Left, PhysicalKey.ArrowLeft);
|
|
RunDispatcherJobsUntil(() => !childContainer.IsSubMenuOpen);
|
|
|
|
childContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
IsActive(parentContainer).ShouldBeFalse();
|
|
IsActive(childContainer).ShouldBeTrue(
|
|
"Inline Left should collapse the active submenu itself instead of moving active state to its parent.");
|
|
IsActive(optionContainer).ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Right_Key_Expands_Active_Submenu_Without_Moving_Keyboard_Active_Item()
|
|
{
|
|
var option = new NavMenuNode
|
|
{
|
|
Header = "Option",
|
|
ItemKey = "option"
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
child.Children.Add(option);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var childContainer = (NavMenuItem)parentContainer.ContainerFromItem(child)!;
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(childContainer).ShouldBeTrue();
|
|
childContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
|
|
PressKey(window, Key.Right, PhysicalKey.ArrowRight);
|
|
RunDispatcherJobsUntil(() => childContainer.IsSubMenuOpen);
|
|
|
|
var optionContainer = (NavMenuItem)childContainer.ContainerFromItem(option)!;
|
|
childContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
IsActive(parentContainer).ShouldBeFalse();
|
|
IsActive(childContainer).ShouldBeTrue(
|
|
"Inline Right should expand the active submenu itself instead of moving active state to the first child.");
|
|
IsActive(optionContainer).ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Key.Left, PhysicalKey.ArrowLeft)]
|
|
[InlineData(Key.Right, PhysicalKey.ArrowRight)]
|
|
public void Inline_Left_And_Right_Keys_Do_Not_Move_Active_Leaf_Item(Key key, PhysicalKey physicalKey)
|
|
{
|
|
var option = new NavMenuNode
|
|
{
|
|
Header = "Option",
|
|
ItemKey = "option"
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
child.Children.Add(option);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var childContainer = (NavMenuItem)parentContainer.ContainerFromItem(child)!;
|
|
childContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var optionContainer = (NavMenuItem)childContainer.ContainerFromItem(option)!;
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(optionContainer).ShouldBeTrue();
|
|
childContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
|
|
PressKey(window, key, physicalKey);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
childContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
IsActive(parentContainer).ShouldBeFalse();
|
|
IsActive(childContainer).ShouldBeFalse();
|
|
IsActive(optionContainer).ShouldBeTrue(
|
|
"Inline Left/Right should only expand or collapse the active item when it has a submenu; leaf active items should not move.");
|
|
menu.SelectedItem.ShouldBeNull();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Inline_Selected_Path_Parent_Shows_Keyboard_Active_Background()
|
|
{
|
|
var option1 = new NavMenuNode
|
|
{
|
|
Header = "Option 1",
|
|
ItemKey = "option-1"
|
|
};
|
|
var option2 = new NavMenuNode
|
|
{
|
|
Header = "Option 2",
|
|
ItemKey = "option-2"
|
|
};
|
|
var item1 = new NavMenuNode
|
|
{
|
|
Header = "Item 1",
|
|
ItemKey = "item-1"
|
|
};
|
|
item1.Children.Add(option1);
|
|
item1.Children.Add(option2);
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(item1);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var item1Container = (NavMenuItem)parentContainer.ContainerFromItem(item1)!;
|
|
item1Container.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var option2Container = (NavMenuItem)item1Container.ContainerFromItem(option2)!;
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(option2Container);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var item1Header = GetItemHeader(item1Container);
|
|
item1Header.IsInSelectedPath.ShouldBeTrue();
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Up, PhysicalKey.ArrowUp);
|
|
PressKey(window, Key.Up, PhysicalKey.ArrowUp);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
item1Container.IsKeyboardActive.ShouldBeTrue();
|
|
item1Header.IsKeyboardActive.ShouldBeTrue();
|
|
GetSolidBrushColor(item1Header.Background).ShouldNotBe(
|
|
Colors.Transparent,
|
|
"A selected-path submenu title must still show keyboard active background; selected path should keep text color, not suppress active feedback.");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Popup_Escape_Closes_Open_Submenu_Without_Clearing_Selected_Item()
|
|
{
|
|
var selected = new NavMenuNode
|
|
{
|
|
Header = "Selected",
|
|
ItemKey = "selected"
|
|
};
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(selected);
|
|
menu.Items.Add(parent);
|
|
|
|
ShowInWindow(CreatePopupOverlayHost(menu), window =>
|
|
{
|
|
var selectedContainer = (NavMenuItem)menu.ContainerFromItem(selected)!;
|
|
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
|
|
|
|
menu.InteractionHandler.ShouldNotBeNull();
|
|
menu.SelectNavMenuItem(selectedContainer);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
parentContainer.Open();
|
|
Dispatcher.UIThread.RunJobs();
|
|
parentContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
menu.SelectedItem.ShouldBeSameAs(selected);
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue(
|
|
"NavMenu itself must be focusable so Escape can close an open keyboard popup branch.");
|
|
PressKey(window, Key.Escape, PhysicalKey.Escape);
|
|
RunDispatcherJobsUntil(() => !parentContainer.IsSubMenuOpen);
|
|
|
|
parentContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeSameAs(selected);
|
|
selectedContainer.IsSelected.ShouldBeTrue();
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Closing_A_Popup_Invalidates_Its_Keyboard_Active_Child_Before_Enter()
|
|
{
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Vertical,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
var selectedNodes = new List<INavMenuNode>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
|
|
ShowInWindow(CreatePopupOverlayHost(menu), window =>
|
|
{
|
|
var parentContainer = menu.ContainerFromItem(parent).ShouldBeOfType<NavMenuItem>();
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
PressKey(window, Key.Right, PhysicalKey.ArrowRight);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
var childContainer = parentContainer.ContainerFromItem(child).ShouldBeOfType<NavMenuItem>();
|
|
parentContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
IsActive(childContainer).ShouldBeTrue();
|
|
|
|
parentContainer.Close();
|
|
RunDispatcherJobsUntil(() => !parentContainer.IsSubMenuOpen);
|
|
|
|
PressKey(window, Key.Enter, PhysicalKey.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeNull();
|
|
selectedNodes.ShouldBeEmpty();
|
|
IsActive(childContainer).ShouldBeFalse();
|
|
IsActive(parentContainer).ShouldBeTrue(
|
|
"Enter should re-establish an active root item instead of committing a child from a closed popup.");
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Horizontal_Down_Key_Enters_Submenu_And_Enter_Commits_Leaf()
|
|
{
|
|
var child = new NavMenuNode
|
|
{
|
|
Header = "Child",
|
|
ItemKey = "child"
|
|
};
|
|
var parent = new NavMenuNode
|
|
{
|
|
Header = "Parent",
|
|
ItemKey = "parent"
|
|
};
|
|
parent.Children.Add(child);
|
|
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Horizontal,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(parent);
|
|
|
|
var selectedNodes = new List<INavMenuNode>();
|
|
menu.NavMenuNodeSelected += (_, args) => selectedNodes.Add(args.NavMenuNode);
|
|
|
|
ShowInWindow(CreatePopupOverlayHost(menu), window =>
|
|
{
|
|
var parentContainer = (NavMenuItem)menu.ContainerFromItem(parent)!;
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(parentContainer).ShouldBeTrue();
|
|
parentContainer.IsSubMenuOpen.ShouldBeFalse();
|
|
menu.SelectedItem.ShouldBeNull();
|
|
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
parentContainer.IsSubMenuOpen.ShouldBeTrue();
|
|
var childContainer = (NavMenuItem)parentContainer.ContainerFromItem(child)!;
|
|
IsActive(parentContainer).ShouldBeFalse();
|
|
IsActive(childContainer).ShouldBeTrue();
|
|
menu.SelectedItem.ShouldBeNull(
|
|
"Entering a submenu with the keyboard must only move keyboard active state; it must not commit selection.");
|
|
|
|
PressKey(window, Key.Enter, PhysicalKey.Enter);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
menu.SelectedItem.ShouldBeSameAs(child);
|
|
selectedNodes.ShouldBe([child]);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void Keyboard_Active_State_Is_Propagated_To_Header()
|
|
{
|
|
var item = new NavMenuNode
|
|
{
|
|
Header = "Item",
|
|
ItemKey = "item"
|
|
};
|
|
var menu = new AtomUI.Desktop.Controls.NavMenu
|
|
{
|
|
Mode = NavMenuMode.Inline,
|
|
IsMotionEnabled = false
|
|
};
|
|
menu.Items.Add(item);
|
|
|
|
ShowInWindow(menu, window =>
|
|
{
|
|
var container = (NavMenuItem)menu.ContainerFromItem(item)!;
|
|
var header = GetItemHeader(container);
|
|
|
|
menu.Focus(NavigationMethod.Tab).ShouldBeTrue();
|
|
PressKey(window, Key.Down, PhysicalKey.ArrowDown);
|
|
Dispatcher.UIThread.RunJobs();
|
|
|
|
IsActive(container).ShouldBeTrue();
|
|
header.IsKeyboardActive.ShouldBeTrue(
|
|
"NavMenuItem keyboard active state must reach the header so the AXAML active visual is driven by the same state.");
|
|
});
|
|
}
|
|
|
|
private static bool IsActive(NavMenuItem container)
|
|
{
|
|
return container.IsKeyboardActive;
|
|
}
|
|
|
|
private static BaseNavMenuItemHeader GetItemHeader(NavMenuItem container)
|
|
{
|
|
container.ItemHeader.ShouldBeAssignableTo<BaseNavMenuItemHeader>();
|
|
return (BaseNavMenuItemHeader)container.ItemHeader!;
|
|
}
|
|
|
|
private static Color? GetSolidBrushColor(IBrush? brush)
|
|
{
|
|
return brush is ISolidColorBrush solidColorBrush
|
|
? solidColorBrush.Color
|
|
: null;
|
|
}
|
|
|
|
private static void PressKey(AvaloniaWindow window, Key key, PhysicalKey physicalKey)
|
|
{
|
|
window.KeyPress(key, RawInputModifiers.None, physicalKey, null);
|
|
}
|
|
|
|
private static void ShowInWindow(Control content, Action<AvaloniaWindow> assertion)
|
|
{
|
|
var window = new AvaloniaWindow
|
|
{
|
|
Width = 360,
|
|
Height = 260,
|
|
Content = content
|
|
};
|
|
|
|
try
|
|
{
|
|
window.Show();
|
|
Dispatcher.UIThread.RunJobs();
|
|
content.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs();
|
|
assertion(window);
|
|
}
|
|
finally
|
|
{
|
|
window.Close();
|
|
}
|
|
}
|
|
|
|
private static void RunDispatcherJobsUntil(Func<bool> condition, int maxPasses = 128)
|
|
{
|
|
for (var i = 0; i < maxPasses && !condition(); i++)
|
|
{
|
|
Dispatcher.UIThread.RunJobs();
|
|
}
|
|
}
|
|
|
|
private static VisualLayerManager CreatePopupOverlayHost(Control content)
|
|
{
|
|
var visualLayerManager = new VisualLayerManager
|
|
{
|
|
Child = content
|
|
};
|
|
|
|
EnablePopupOverlayLayer(visualLayerManager);
|
|
return visualLayerManager;
|
|
}
|
|
|
|
private static void EnablePopupOverlayLayer(VisualLayerManager visualLayerManager)
|
|
{
|
|
var property = typeof(VisualLayerManager).GetProperty(
|
|
"EnablePopupOverlayLayer",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
|
|
property.ShouldNotBeNull();
|
|
property.SetValue(visualLayerManager, true);
|
|
}
|
|
}
|