fix(border-color): keep ComboBox popup clicks inside LeftPanel (TASK 63)

This commit is contained in:
2026-10-22 12:02:05 -07:00
parent daac7689fd
commit c768a8ee49
8 changed files with 241 additions and 87 deletions
+98
View File
@@ -132,9 +132,107 @@ public sealed class BorderColorPickerTests
$"the drop-down rows do not show the colour's name: '{rows}'");
window.Close();
// ── 4. clicking a row inside the open popup stays inside LeftPanel ───────
// MainWindow_PreviewMouseLeftButtonDown deselects SelectedElement (exiting
// the properties listing) whenever LeftPanel.IsClickInside(e.OriginalSource)
// returns false. A ComboBox's PART_Popup lives in its own PopupRoot visual
// tree whose VisualTreeHelper.GetParent is null — the only link back to the
// ComboBox and ElementsScrollViewer is PopupRoot's LOGICAL parent.
panel.DataContext = new PanelContext { SelectedElement = element };
var hostWindow = new Window { Width = 320, Height = 720, Content = panel };
hostWindow.Show();
panel.UpdateLayout();
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded);
var realCombo = FindVisualChildren<ComboBox>(panel)
.Single(c => c.ItemsSource is IEnumerable<NamedColor> opts && opts.Any(o => o.Hex == "#e94560"));
realCombo.IsDropDownOpen = true;
realCombo.UpdateLayout();
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded);
var crimsonEntry = BorderColorPalette.EntryFor("#e94560");
var crimsonItem = realCombo.ItemContainerGenerator.ContainerFromItem(crimsonEntry) as ComboBoxItem;
Assert.NotNull(crimsonItem);
// The click target inside the popup row (e.g. a TextBlock inside the item)
// must be recognised as inside LeftPanel, or MainWindow clears SelectedElement.
var clickTarget = FindVisualChildren<TextBlock>(crimsonItem!).FirstOrDefault()
?? (DependencyObject)crimsonItem!;
Assert.True(panel.IsClickInside(clickTarget),
"clicking a colour row in the open ComboBox popup is treated as a click outside LeftPanel (exits properties)");
// Clicking the row in the open popup updates the webcam's BorderColor and closes the popup.
crimsonItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
{
RoutedEvent = UIElement.MouseLeftButtonDownEvent,
Source = clickTarget,
});
crimsonItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
{
RoutedEvent = UIElement.MouseLeftButtonUpEvent,
Source = clickTarget,
});
realCombo.UpdateLayout();
Assert.Equal("#e94560", element.BorderColor);
Assert.Equal(crimsonEntry, realCombo.SelectedItem);
Assert.Contains("Crimson", Texts(realCombo));
Assert.False(realCombo.IsDropDownOpen);
// And when starting from a typed custom hex (#123456), picking a palette
// row in the open popup still lands cleanly on that palette colour.
element.BorderColor = "#123456";
realCombo.IsDropDownOpen = true;
realCombo.UpdateLayout();
Dispatcher.CurrentDispatcher.Invoke(() => { }, DispatcherPriority.Loaded);
var emeraldEntry = BorderColorPalette.EntryFor("#22c55e");
var emeraldItem = realCombo.ItemContainerGenerator.ContainerFromItem(emeraldEntry) as ComboBoxItem;
Assert.NotNull(emeraldItem);
var emeraldTarget = FindVisualChildren<TextBlock>(emeraldItem!).FirstOrDefault()
?? (DependencyObject)emeraldItem!;
Assert.True(panel.IsClickInside(emeraldTarget));
emeraldItem!.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
{
RoutedEvent = UIElement.MouseLeftButtonDownEvent,
Source = emeraldTarget,
});
emeraldItem.RaiseEvent(new System.Windows.Input.MouseButtonEventArgs(
System.Windows.Input.Mouse.PrimaryDevice, 0, System.Windows.Input.MouseButton.Left)
{
RoutedEvent = UIElement.MouseLeftButtonUpEvent,
Source = emeraldTarget,
});
realCombo.UpdateLayout();
Assert.Equal("#22c55e", element.BorderColor);
Assert.Equal(emeraldEntry, realCombo.SelectedItem);
Assert.Contains("Emerald", Texts(realCombo));
hostWindow.Close();
});
}
private sealed class PanelContext
{
public SceneElement? SelectedElement { get; set; }
public bool IsWebcamSelected => true;
public bool CanToggleClipShape => true;
}
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject root) where T : DependencyObject
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is T match) yield return match;
foreach (var nested in FindVisualChildren<T>(child))
yield return nested;
}
}
/// <summary>Every text in a visual tree, so a template's content can be read back.</summary>
private static string Texts(DependencyObject root)
{