diff --git a/build/Versions.props b/build/Versions.props index 6e71cb77d..82484d8df 100644 --- a/build/Versions.props +++ b/build/Versions.props @@ -1,6 +1,6 @@ 12.1.1 - 6.1.3 + 6.1.4 diff --git a/docs/controls/desktop/navigation/combo-box/changelog.md b/docs/controls/desktop/navigation/combo-box/changelog.md index b72b85b9a..5aa94a96f 100644 --- a/docs/controls/desktop/navigation/combo-box/changelog.md +++ b/docs/controls/desktop/navigation/combo-box/changelog.md @@ -2,6 +2,11 @@ 本文档记录 ComboBox 控件级设计、API、主题契约、Token 和实现结构的变化。它不替代仓库根目录 `CHANGELOG.md`,也不作为正式版本发布说明。 +## 2026-08-18 + +- Behavior + - Keep keyboard input on a non-editable ComboBox when reopening its popup so consecutive `Down` and `Enter` selections continue to update the selected item. + ## 2026-07-05 - Behavior diff --git a/src/AtomUI.Desktop.Controls/ComboBox/ComboBox.cs b/src/AtomUI.Desktop.Controls/ComboBox/ComboBox.cs index 193b66e2b..97b196125 100644 --- a/src/AtomUI.Desktop.Controls/ComboBox/ComboBox.cs +++ b/src/AtomUI.Desktop.Controls/ComboBox/ComboBox.cs @@ -910,7 +910,16 @@ public class ComboBox : AvaloniaComboBox, private void HandlePopupOpened(object? sender, EventArgs e) { RefreshFilteredItemVisibility(); - FocusEditableTextBox(NavigationMethod.Unspecified); + + if (IsEditable) + { + FocusEditableTextBox(NavigationMethod.Unspecified); + } + else if (TopLevel.GetTopLevel(this)?.FocusManager.GetFocusedElement() is Visual focusedVisual && + _popup?.IsInsidePopup(focusedVisual) == true) + { + Focus(NavigationMethod.Unspecified); + } } private void HandleEditableTextBoxTextInput(TextInputEventArgs e) diff --git a/tests/AtomUI.Desktop.Controls.Tests/ComboBox/ComboBoxKeyboardNavigationTests.cs b/tests/AtomUI.Desktop.Controls.Tests/ComboBox/ComboBoxKeyboardNavigationTests.cs new file mode 100644 index 000000000..80f86be4b --- /dev/null +++ b/tests/AtomUI.Desktop.Controls.Tests/ComboBox/ComboBoxKeyboardNavigationTests.cs @@ -0,0 +1,128 @@ +using System.Reflection; +using AtomUI.Controls; +using AtomUI.Controls.Primitives; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Headless; +using Avalonia.Input; +using Avalonia.Threading; +using Shouldly; +using Xunit; +using AvaloniaWindow = Avalonia.Controls.Window; +using AtomUIComboBox = AtomUI.Desktop.Controls.ComboBox; + +namespace AtomUI.Desktop.Controls.Tests.ComboBox; + +public class ComboBoxKeyboardNavigationTests +{ + static ComboBoxKeyboardNavigationTests() + { + AvaloniaTestApp.EnsureInitialized(); + } + + [Fact] + public void NonEditable_ComboBox_Allows_Consecutive_Keyboard_Selections() + { + var comboBox = new AtomUIComboBox + { + Width = 240, + ItemsSource = new[] { "Alpha", "Beta", "Gamma" }, + IsMotionEnabled = false + }; + + ShowInWindow(comboBox, window => + { + comboBox.Focus(NavigationMethod.Tab).ShouldBeTrue(); + Dispatcher.UIThread.RunJobs(); + + SelectNextItem(window, comboBox, 0); + + comboBox.Focus(NavigationMethod.Tab).ShouldBeTrue(); + comboBox.IsDropDownOpen = true; + Dispatcher.UIThread.RunJobs(); + + TopLevel.GetTopLevel(comboBox)?.FocusManager.GetFocusedElement() + .ShouldBeSameAs(comboBox); + + PressKey(window, Key.Down, PhysicalKey.ArrowDown); + Dispatcher.UIThread.RunJobs(); + PressKey(window, Key.Enter, PhysicalKey.Enter); + Dispatcher.UIThread.RunJobs(); + + comboBox.SelectedIndex.ShouldBe(1); + comboBox.IsDropDownOpen.ShouldBeFalse(); + }); + } + + private static void SelectNextItem(AvaloniaWindow window, AtomUIComboBox comboBox, int expectedIndex) + { + comboBox.IsDropDownOpen = true; + Dispatcher.UIThread.RunJobs(); + + PressKey(window, Key.Down, PhysicalKey.ArrowDown); + Dispatcher.UIThread.RunJobs(); + PressKey(window, Key.Enter, PhysicalKey.Enter); + Dispatcher.UIThread.RunJobs(); + + comboBox.SelectedIndex.ShouldBe(expectedIndex); + comboBox.IsDropDownOpen.ShouldBeFalse(); + } + + private static void PressKey(AvaloniaWindow window, Key key, PhysicalKey physicalKey) + { + window.KeyPress(key, RawInputModifiers.None, physicalKey, null); + } + + private static void ShowInWindow(Control content, Action assertion) + { + var window = new AvaloniaWindow + { + Width = 360, + Height = 220, + Content = CreatePopupOverlayHost(content) + }; + + try + { + window.Show(); + Dispatcher.UIThread.RunJobs(); + content.ApplyTemplate(); + content.Measure(Size.Infinity); + content.Arrange(new Rect(content.DesiredSize)); + Dispatcher.UIThread.RunJobs(); + assertion(window); + } + finally + { + window.Close(); + } + } + + private static VisualLayerManager CreatePopupOverlayHost(Control content) + { + var overlayPanel = new ScopeAwareOverlayLayerPanel + { + Width = 360, + Height = 220 + }; + overlayPanel.Children.Add(content); + + var visualLayerManager = new VisualLayerManager + { + Child = overlayPanel + }; + EnablePopupOverlayLayer(visualLayerManager); + return visualLayerManager; + } + + private static void EnablePopupOverlayLayer(VisualLayerManager visualLayerManager) + { + var property = typeof(VisualLayerManager).GetProperty( + "EnablePopupOverlayLayer", + BindingFlags.Instance | BindingFlags.NonPublic); + + property.ShouldNotBeNull(); + property.SetValue(visualLayerManager, true); + } +}