diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml
new file mode 100644
index 0000000000..7b820b0183
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml
@@ -0,0 +1,121 @@
+
+
+ M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z
+ M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z
+ M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z
+ M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19C20.92,17.39 19.61,16.08 18,16.08Z
+ M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keyboard navigation in the overflow popup: Up/Down to move, Home/End to jump, Escape to close.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs
new file mode 100644
index 0000000000..013614db7f
--- /dev/null
+++ b/samples/ControlCatalog/Pages/CommandBar/CommandBarKeyboardPage.xaml.cs
@@ -0,0 +1,93 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+
+namespace ControlCatalog.Pages
+{
+ public partial class CommandBarKeyboardPage : UserControl
+ {
+ private readonly List _log = new();
+
+ public CommandBarKeyboardPage()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ Unloaded += OnUnloaded;
+ }
+
+ private void OnLoaded(object? sender, RoutedEventArgs e)
+ {
+ DemoBar.Opened += OnOpened;
+ DemoBar.Closed += OnClosed;
+
+ BtnCopy.GotFocus += OnItemFocused;
+ BtnPaste.GotFocus += OnItemFocused;
+ BtnBold.GotFocus += OnItemFocused;
+ BtnShare.GotFocus += OnItemFocused;
+ BtnDelete.GotFocus += OnItemFocused;
+ BtnExport.GotFocus += OnItemFocused;
+ }
+
+ private void OnUnloaded(object? sender, RoutedEventArgs e)
+ {
+ DemoBar.Opened -= OnOpened;
+ DemoBar.Closed -= OnClosed;
+
+ BtnCopy.GotFocus -= OnItemFocused;
+ BtnPaste.GotFocus -= OnItemFocused;
+ BtnBold.GotFocus -= OnItemFocused;
+ BtnShare.GotFocus -= OnItemFocused;
+ BtnDelete.GotFocus -= OnItemFocused;
+ BtnExport.GotFocus -= OnItemFocused;
+ }
+
+ private void OnOpened(object? sender, RoutedEventArgs e)
+ => AppendLog("Opened. Use arrow keys to navigate.");
+
+ private void OnClosed(object? sender, RoutedEventArgs e)
+ => AppendLog("Closed");
+
+ private void OnItemFocused(object? sender, FocusChangedEventArgs e)
+ {
+ var label = sender switch
+ {
+ CommandBarButton btn => btn.Label ?? "(unnamed)",
+ CommandBarToggleButton t => t.Label ?? "(unnamed)",
+ _ => sender?.GetType().Name ?? "?"
+ };
+
+ var method = e.NavigationMethod switch
+ {
+ NavigationMethod.Directional => "arrow key",
+ NavigationMethod.Tab => "Tab",
+ NavigationMethod.Pointer => "pointer",
+ _ => "unspecified"
+ };
+
+ AppendLog($"Focus: {label} ({method})");
+ }
+
+ private void OnOpenOverflow(object? sender, RoutedEventArgs e)
+ {
+ DemoBar.IsOpen = true;
+ }
+
+ private void OnClearLog(object? sender, RoutedEventArgs e)
+ {
+ _log.Clear();
+ FocusLogText.Text = "Log cleared.";
+ }
+
+ private void AppendLog(string message)
+ {
+ _log.Add(message);
+
+ if (_log.Count > 10)
+ _log.RemoveAt(0);
+
+ FocusLogText.Text = string.Join("\n", _log.Select((entry, i) => $"{i + 1,2}. {entry}"));
+ }
+ }
+}
diff --git a/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs b/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
index e76d605645..b759c7b605 100644
--- a/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/CommandBarPage.xaml.cs
@@ -23,6 +23,7 @@ namespace ControlCatalog.Pages
("Features", "Overflow Menu", "Secondary commands appear in an overflow popup. Configure visibility and sticky behavior.", () => new CommandBarOverflowPage()),
("Features", "Dynamic Overflow", "IsDynamicOverflowEnabled moves primary commands to overflow as space shrinks.", () => new CommandBarDynamicOverflowPage()),
("Features", "Events & State", "Observe Opening, Opened, Closing, and Closed while tracking IsOpen, HasSecondaryCommands, and IsOverflowButtonVisible.", () => new CommandBarEventsPage()),
+ ("Features", "Keyboard Navigation", "Up/Down to move between overflow items, Home/End to jump to first/last, Escape to close and return focus.", () => new CommandBarKeyboardPage()),
};
public CommandBarPage()
diff --git a/src/Avalonia.Controls/CommandBar/CommandBar.cs b/src/Avalonia.Controls/CommandBar/CommandBar.cs
index d493bce304..d8047f9d9e 100644
--- a/src/Avalonia.Controls/CommandBar/CommandBar.cs
+++ b/src/Avalonia.Controls/CommandBar/CommandBar.cs
@@ -7,6 +7,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Metadata;
+using Avalonia.Threading;
namespace Avalonia.Controls
{
@@ -14,9 +15,10 @@ namespace Avalonia.Controls
/// A command bar that provides primary commands displayed inline and secondary commands
/// accessible via an overflow menu.
///
- [TemplatePart("PART_OverflowButton", typeof(Button))]
+ [TemplatePart("PART_OverflowButton", typeof(Button))]
[TemplatePart("PART_OverflowPopup", typeof(Popup))]
- [TemplatePart("PART_ContentPresenter", typeof(Control))]
+ [TemplatePart("PART_OverflowPresenter", typeof(ItemsControl))]
+ [TemplatePart("PART_ContentPresenter", typeof(Control))]
public class CommandBar : TemplatedControl
{
///
@@ -129,12 +131,14 @@ namespace Avalonia.Controls
private Button? _overflowButton;
private Popup? _overflowPopup;
+ private ItemsControl? _overflowPresenter;
private Control? _contentPresenter;
private readonly ObservableCollection _visiblePrimaryCommands = new();
private readonly ObservableCollection _overflowItems = new();
private bool _isDynamicUpdateInProgress;
private double _constraintWidth = double.PositiveInfinity;
+ private bool _openedViaKeyboard;
public CommandBar()
{
@@ -348,14 +352,33 @@ namespace Avalonia.Controls
base.OnApplyTemplate(e);
if (_overflowButton != null)
+ {
_overflowButton.Click -= OnOverflowButtonClick;
+ _overflowButton.GotFocus -= OnOverflowButtonGotFocus;
+ _overflowButton.RemoveHandler(Input.InputElement.KeyDownEvent, OnOverflowButtonKeyDown);
+ _overflowButton.RemoveHandler(Input.InputElement.PointerPressedEvent, OnOverflowButtonPointerPressed);
+ }
+ if (_overflowPresenter != null)
+ _overflowPresenter.KeyDown -= OnOverflowPresenterKeyDown;
+ if (_overflowPopup != null)
+ _overflowPopup.Opened -= OnOverflowPopupOpened;
_overflowButton = e.NameScope.Find