1
0
Эх сурвалжийг харах

Fixes and improves several access key (accelerator) related issues (#17292)

* fix accelerator behavior for menu items and labels

* add elements with matching accelerator to test cycling in sub menus

* Add AccessKeyHandler tests for accelerators with more than one match

* Implement accelerator behavior based on WPF handling

* Remove commented code

* Remove OnAccessKey override => handled by DefaultMenuInteractionHandler

* remove obsolete test

* handle OnAccessKeyPressed for selected tab item

* fix unit tests

* use AccessKeyEvent instead of AccessKeyPressedEvent in unit tests

* navigate menu with and without ALT key

* Revert formatting changes in Tests

* Fix AccessKeyHandler comments

* move private types to bottom

* Remove lock statements, optimize removal of AccessKeyRegistrations

* remove call to Dispatcher.UIThread.Post

* simplifiy AccessKeyHandler.SortByHierarchy

* remove unnecessary method AccessKeyHandler.GetTargetsForSender

* regenerate API suppression file

* revert unneeded changes in MenuPage.axaml

* correct formatting changes

* do not sort by hierarchy if too few targets

* make AccessKeyEventArgs internal

* make AccessKeyPressedEventArgs internal

---------

Co-authored-by: Hans Docsek <[email protected]>
StefanKoell 11 сар өмнө
parent
commit
b46126714b

+ 6 - 0
api/Avalonia.nupkg.xml

@@ -91,4 +91,10 @@
     <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0012</DiagnosticId>
+    <Target>M:Avalonia.Controls.Button.OnAccessKey(Avalonia.Interactivity.RoutedEventArgs)</Target>
+    <Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
+    <Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
+  </Suppression>
 </Suppressions>

+ 3 - 0
samples/ControlCatalog/MainView.xaml

@@ -18,6 +18,9 @@
       <TabItem Header="Composition">
         <pages:CompositionPage />
       </TabItem>
+      <TabItem Header="Accelerator">
+        <pages:AcceleratorPage />
+      </TabItem>
       <TabItem Header="Acrylic">
         <pages:AcrylicPage />
       </TabItem>

+ 115 - 0
samples/ControlCatalog/Pages/AcceleratorPage.xaml

@@ -0,0 +1,115 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             x:Class="ControlCatalog.Pages.AcceleratorPage">
+  <StackPanel Orientation="Vertical" Spacing="4">
+
+    <WrapPanel HorizontalAlignment="Left">
+      <StackPanel>
+        <Menu>
+          <MenuItem Header="_First">
+            <MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A" />
+            <MenuItem Header="_Disabled Menu Item" IsEnabled="False" InputGesture="Ctrl+D" />
+            <Separator />
+            <MenuItem Header="Menu with Sub _Menu">
+              <MenuItem Header="Submenu _1" />
+              <MenuItem Header="Submenu _2 with Submenu">
+                <MenuItem Header="Submenu Level 2" />
+              </MenuItem>
+              <MenuItem Header="Submenu _3 with Submenu Disabled" IsEnabled="False">
+                <MenuItem Header="Submenu Level 2" />
+              </MenuItem>
+            </MenuItem>
+            <MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
+              <MenuItem.Icon>
+                <Image Source="/Assets/github_icon.png" />
+              </MenuItem.Icon>
+            </MenuItem>
+            <MenuItem Header="Menu Item with _Checkbox" ToggleType="CheckBox" />
+          </MenuItem>
+          <MenuItem Header="_Second">
+            <MenuItem Header="Second _Menu Item" />
+            <MenuItem IsChecked="True" Header="Second _Menu toggle item" ToggleType="CheckBox" />
+            <Separator />
+            <MenuItem GroupName="A" Header="Radio 1 - group" ToggleType="Radio" />
+            <MenuItem IsChecked="True" GroupName="A" Header="Radio 2 - group" ToggleType="Radio" />
+            <MenuItem GroupName="A" Header="Radio 3 - group" ToggleType="Radio">
+              <MenuItem Header="Radio 4 - group" ToggleType="Radio" GroupName="A" />
+              <MenuItem Header="Radio 5 - group" ToggleType="Radio" GroupName="A" />
+            </MenuItem>
+            <Separator />
+            <MenuItem Header="Radio 1" ToggleType="Radio" />
+            <MenuItem IsChecked="True" Header="Radio 2" ToggleType="Radio" />
+            <MenuItem Header="Radio 3" ToggleType="Radio">
+              <MenuItem Header="Radio 4" ToggleType="Radio" />
+              <MenuItem Header="Radio 5" ToggleType="Radio" />
+            </MenuItem>
+          </MenuItem>
+          <MenuItem Header="Thir_d">
+            <MenuItem Header="About"/>
+            <MenuItem Header="_Child">
+              <MenuItem Header="_Grandchild"/>
+            </MenuItem>
+          </MenuItem>
+        </Menu>
+      </StackPanel>
+
+    </WrapPanel>
+
+    <StackPanel Spacing="10">
+      <TextBlock Classes="h2">Accelerator Support</TextBlock>
+
+      <TabControl Margin="10" BorderBrush="Gray" BorderThickness="1">
+        <TabItem Header="_Tab 1">
+          <StackPanel>
+            <TextBlock Margin="5">This is tab 1 content</TextBlock>
+            <Label Name="Tab1Label1" Target="Tab1TextBox1">_Label Tab1Label1</Label>
+            <TextBox Name="Tab1TextBox1" Margin="5">This is tab 1 content</TextBox>
+            <Label Name="Tab1Label2" Target="Tab1TextBox2">Label _Tab1Label2</Label>
+            <TextBox Name="Tab1TextBox2" Margin="5">This is tab 1 content</TextBox>
+          </StackPanel>
+
+        </TabItem>
+        <TabItem Header="T_ab 2">
+          <TextBlock Margin="5">This is tab 2 content</TextBlock>
+        </TabItem>
+        <TabItem Header="_Tab 3">
+
+        </TabItem>
+        <TabItem Header="_Tab 4">
+          <TextBlock Margin="5">This is tab 4 content</TextBlock>
+        </TabItem>
+        <TabItem Header="_Fab 5">
+          <TextBlock Margin="5">This is fab 5 content</TextBlock>
+        </TabItem>
+      </TabControl>
+    </StackPanel>
+
+
+    <StackPanel Spacing="10">
+      <Label Name="Label0">Label with Ac_celerator 'C' and no Target</Label>
+      <TextBox Name="TextBox0" Text="Some Text"></TextBox>
+
+      <Label Name="Label1" Target="TextBox1">_Label with Accelerator 'L'</Label>
+      <TextBox Name="TextBox1" Text="Some Text"></TextBox>
+
+      <Label Name="Label2" Target="TextBox2">La_bel with Accelerator 'B'</Label>
+      <TextBox Name="TextBox2" Text="Some Text"></TextBox>
+
+      <Label Name="Label3" Target="TextBox3">L_abel with Accelerator 'A'</Label>
+      <TextBox Name="TextBox3" Text="Some Text"></TextBox>
+
+      <Label Name="Label4" Target="TextBox4">La_bel with Accelerator 'B'</Label>
+      <TextBox Name="TextBox4" Text="Some Text"></TextBox>
+
+      <Label Name="Label5" Target="TextBox5">_Flabel with Accelerator 'F' (Same as in Menu > File)</Label>
+      <TextBox Name="TextBox5" Text="Some Text"></TextBox>
+
+    </StackPanel>
+
+    <StackPanel Spacing="10" Orientation="Horizontal">
+      <Button Name="Button1">_Button 1</Button>
+      <Button Name="Button2">_Button 2</Button>
+      <Button Name="Button3">_Button 3</Button>
+    </StackPanel>
+  </StackPanel>
+</UserControl>

+ 18 - 0
samples/ControlCatalog/Pages/AcceleratorPage.xaml.cs

@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace ControlCatalog.Pages
+{
+    public class AcceleratorPage : UserControl
+    {
+        public AcceleratorPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 365 - 43
src/Avalonia.Base/Input/AccessKeyHandler.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Input
 {
@@ -11,11 +12,20 @@ namespace Avalonia.Input
     /// </summary>
     internal class AccessKeyHandler : IAccessKeyHandler
     {
+        /// <summary>
+        /// Defines the AccessKey attached event.
+        /// </summary>
+        public static readonly RoutedEvent<AccessKeyEventArgs> AccessKeyEvent =
+            RoutedEvent.Register<AccessKeyEventArgs>(
+                "AccessKey",
+                RoutingStrategies.Bubble,
+                typeof(AccessKeyHandler));
+
         /// <summary>
         /// Defines the AccessKeyPressed attached event.
         /// </summary>
-        public static readonly RoutedEvent<RoutedEventArgs> AccessKeyPressedEvent =
-            RoutedEvent.Register<RoutedEventArgs>(
+        public static readonly RoutedEvent<AccessKeyPressedEventArgs> AccessKeyPressedEvent =
+            RoutedEvent.Register<AccessKeyPressedEventArgs>(
                 "AccessKeyPressed",
                 RoutingStrategies.Bubble,
                 typeof(AccessKeyHandler));
@@ -23,7 +33,9 @@ namespace Avalonia.Input
         /// <summary>
         /// The registered access keys.
         /// </summary>
-        private readonly List<(string AccessKey, IInputElement Element)> _registered = new();
+        private readonly List<AccessKeyRegistration> _registrations = [];
+
+        protected IReadOnlyList<AccessKeyRegistration> Registrations => _registrations;
 
         /// <summary>
         /// The window to which the handler belongs.
@@ -48,7 +60,7 @@ namespace Avalonia.Input
         /// <summary>
         /// Element to restore following AltKey taking focus.
         /// </summary>
-        private IInputElement? _restoreFocusElement;
+        private WeakReference<IInputElement>? _restoreFocusElementRef;
 
         /// <summary>
         /// The window's main menu.
@@ -97,6 +109,12 @@ namespace Avalonia.Input
             _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble);
             _owner.AddHandler(InputElement.KeyUpEvent, OnPreviewKeyUp, RoutingStrategies.Tunnel);
             _owner.AddHandler(InputElement.PointerPressedEvent, OnPreviewPointerPressed, RoutingStrategies.Tunnel);
+
+            OnSetOwner(owner);
+        }
+
+        protected virtual void OnSetOwner(IInputRoot owner)
+        {
         }
 
         /// <summary>
@@ -106,14 +124,19 @@ namespace Avalonia.Input
         /// <param name="element">The input element.</param>
         public void Register(char accessKey, IInputElement element)
         {
-            var existing = _registered.FirstOrDefault(x => x.Item2 == element);
-
-            if (existing != default)
+            var key = NormalizeKey(accessKey.ToString());
+            
+            // remove dead elements with matching key
+            for (var i = _registrations.Count - 1; i >= 0; i--)
             {
-                _registered.Remove(existing);
+                var registration = _registrations[i];
+                if (registration.Key == key && registration.GetInputElement() == null)
+                {
+                    _registrations.RemoveAt(i);    
+                }
             }
 
-            _registered.Add((accessKey.ToString().ToUpperInvariant(), element));
+            _registrations.Add(new AccessKeyRegistration(key, new WeakReference<IInputElement>(element)));
         }
 
         /// <summary>
@@ -122,9 +145,15 @@ namespace Avalonia.Input
         /// <param name="element">The input element.</param>
         public void Unregister(IInputElement element)
         {
-            foreach (var i in _registered.Where(x => x.Item2 == element).ToList())
+            // remove element and all dead elements
+            for (var i = _registrations.Count - 1; i >= 0; i--)
             {
-                _registered.Remove(i);
+                var registration = _registrations[i];
+                var inputElement = registration.GetInputElement();
+                if (inputElement == null || inputElement == element)
+                {
+                    _registrations.RemoveAt(i);    
+                }
             }
         }
 
@@ -135,21 +164,29 @@ namespace Avalonia.Input
         /// <param name="e">The event args.</param>
         protected virtual void OnPreviewKeyDown(object? sender, KeyEventArgs e)
         {
-            if (e.Key == Key.LeftAlt || e.Key == Key.RightAlt)
+            // if the owner (IInputRoot) does not have the keyboard focus, ignore all keyboard events
+            // KeyboardDevice.IsKeyboardFocusWithin in case of a PopupRoot seems to only work once, so we created our own
+            var isFocusWithinOwner = IsFocusWithinOwner(_owner!);
+            if (!isFocusWithinOwner)
+                return;
+
+            if (e.Key is Key.LeftAlt or Key.RightAlt)
             {
                 _altIsDown = true;
 
-                if (MainMenu == null || !MainMenu.IsOpen)
+                if (MainMenu is not { IsOpen: true })
                 {
                     var focusManager = FocusManager.GetFocusManager(e.Source as IInputElement);
-                    
+
                     // TODO: Use FocusScopes to store the current element and restore it when context menu is closed.
                     // Save currently focused input element.
-                    _restoreFocusElement = focusManager?.GetFocusedElement();
+                    var focusedElement = focusManager?.GetFocusedElement();
+                    if (focusedElement is not null)
+                        _restoreFocusElementRef = new WeakReference<IInputElement>(focusedElement);
 
                     // When Alt is pressed without a main menu, or with a closed main menu, show
                     // access key markers in the window (i.e. "_File").
-                    _owner!.ShowAccessKeys = _showingAccessKeys = true;
+                    _owner!.ShowAccessKeys = _showingAccessKeys = isFocusWithinOwner;
                 }
                 else
                 {
@@ -157,8 +194,11 @@ namespace Avalonia.Input
                     CloseMenu();
                     _ignoreAltUp = true;
 
-                    _restoreFocusElement?.Focus();
-                    _restoreFocusElement = null;
+                    if (_restoreFocusElementRef?.TryGetTarget(out var restoreElement) ?? false)
+                    {
+                        restoreElement.Focus();
+                    }
+                    _restoreFocusElementRef = null;
                 }
             }
             else if (_altIsDown)
@@ -174,35 +214,20 @@ namespace Avalonia.Input
         /// <param name="e">The event args.</param>
         protected virtual void OnKeyDown(object? sender, KeyEventArgs e)
         {
-            bool menuIsOpen = MainMenu?.IsOpen == true;
+            // if the owner (IInputRoot) does not have the keyboard focus, ignore all keyboard events
+            // KeyboardDevice.IsKeyboardFocusWithin in case of a PopupRoot seems to only work once, so we created our own
+            var isFocusWithinOwner = IsFocusWithinOwner(_owner!);
+            if (!isFocusWithinOwner)
+                return;
 
-            if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) && !e.KeyModifiers.HasAllFlags(KeyModifiers.Control) || menuIsOpen)
-            {
-                // If any other key is pressed with the Alt key held down, or the main menu is open,
-                // find all controls who have registered that access key.
-                var text = e.Key.ToString();
-                var matches = _registered
-                    .Where(x => string.Equals(x.AccessKey, text, StringComparison.OrdinalIgnoreCase)
-                        && x.Element.IsEffectivelyVisible
-                        && x.Element.IsEffectivelyEnabled)
-                    .Select(x => x.Element);
-
-                // If the menu is open, only match controls in the menu's visual tree.
-                if (menuIsOpen)
-                {
-                    matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsLogicalAncestorOf((Visual)x));
-                }
-
-                var match = matches.FirstOrDefault();
+            if ((!e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || e.KeyModifiers.HasAllFlags(KeyModifiers.Control)) &&
+                MainMenu?.IsOpen != true)
+                return;
 
-                // If there was a match, raise the AccessKeyPressed event on it.
-                if (match is not null)
-                {
-                    match.RaiseEvent(new RoutedEventArgs(AccessKeyPressedEvent));
-                }
-            }
+            e.Handled = ProcessKey(e.Key.ToString(), e.Source as IInputElement);
         }
 
+
         /// <summary>
         /// Handles the Alt/F10 keys being released in the window.
         /// </summary>
@@ -255,5 +280,302 @@ namespace Avalonia.Input
         {
             _owner!.ShowAccessKeys = false;
         }
+
+        /// <summary>
+        /// Processes the given key for the element's targets 
+        /// </summary>
+        /// <param name="key">The access key to process.</param>
+        /// <param name="element">The element to get the targets which are in scope.</param>
+        /// <returns>If there matches <c>true</c>, otherwise <c>false</c>.</returns>
+        protected bool ProcessKey(string key, IInputElement? element)
+        {
+            key = NormalizeKey(key);
+            var senderInfo = GetTargetForElement(element, key);
+            // Find the possible targets matching the access key
+            var targets = SortByHierarchy(GetTargetsForKey(key, element, senderInfo));
+            var result = ProcessKey(key, targets);
+            return result != ProcessKeyResult.NoMatch;
+        }
+
+        private static string NormalizeKey(string key) => key.ToUpperInvariant();
+
+        private static ProcessKeyResult ProcessKey(string key, List<IInputElement> targets)
+        {
+            if (!targets.Any())
+                return ProcessKeyResult.NoMatch;
+
+            var isSingleTarget = true;
+            var lastWasFocused = false;
+
+            IInputElement? effectiveTarget = null;
+
+            var chosenIndex = 0;
+            for (var i = 0; i < targets.Count; i++)
+            {
+                var target = targets[i];
+
+                if (!IsTargetable(target))
+                    continue;
+
+                if (effectiveTarget == null)
+                {
+                    effectiveTarget = target;
+                    chosenIndex = i;
+                }
+                else
+                {
+                    if (lastWasFocused)
+                    {
+                        effectiveTarget = target;
+                        chosenIndex = i;
+                    }
+
+                    isSingleTarget = false;
+                }
+
+                lastWasFocused = target.IsFocused;
+            }
+
+            if (effectiveTarget == null)
+                return ProcessKeyResult.NoMatch;
+
+            var args = new AccessKeyEventArgs(key, isMultiple: !isSingleTarget);
+            effectiveTarget.RaiseEvent(args);
+
+            return chosenIndex == targets.Count - 1 ? ProcessKeyResult.LastMatch : ProcessKeyResult.MoreMatches;
+        }
+
+        private List<IInputElement> GetTargetsForKey(string key, IInputElement? sender,
+            AccessKeyInformation senderInfo)
+        {
+            var possibleElements = CopyMatchingAndPurgeDead(key);
+
+            if (!possibleElements.Any())
+                return possibleElements;
+
+            var finalTargets = new List<IInputElement>(1);
+
+            // Go through all the possible elements, find the interesting candidates
+            foreach (var element in possibleElements)
+            {
+                if (element != sender)
+                {
+                    if (!IsTargetable(element))
+                        continue;
+
+                    var elementInfo = GetTargetForElement(element, key);
+                    if (elementInfo.Target == null)
+                        continue;
+
+                    finalTargets.Add(elementInfo.Target);
+                }
+                else
+                {
+                    // This is the same element that sent the event so it must be in the same scope.
+                    // Just add it to the final targets
+                    if (senderInfo.Target == null)
+                        continue;
+
+                    finalTargets.Add(senderInfo.Target);
+                }
+            }
+
+            return finalTargets;
+        }
+
+        private static bool IsTargetable(IInputElement element) =>
+            element is { IsEffectivelyEnabled: true, IsEffectivelyVisible: true };
+
+        private List<IInputElement> CopyMatchingAndPurgeDead(string key)
+        {
+            var matches = new List<IInputElement>(_registrations.Count);
+                
+            // collect live elements with matching key and remove dead elements
+            for (var i = _registrations.Count - 1; i >= 0; i--)
+            {
+                var registration = _registrations[i];
+                var inputElement = registration.GetInputElement();
+                if (inputElement != null)
+                {
+                    if (registration.Key == key)
+                    {
+                        matches.Add(inputElement);
+                    }
+                }
+                else
+                {
+                    _registrations.RemoveAt(i);
+                }
+            }
+
+            // since we collected the elements when iterating from back to front
+            // we need to reverse them to ensure the original order
+            matches.Reverse();
+            
+            return matches;
+        }
+
+        /// <summary>
+        /// Returns targeting information for the given element.
+        /// </summary>
+        /// <param name="element"></param>
+        /// <param name="key"></param>
+        /// <returns>AccessKeyInformation with target for the access key.</returns>
+        private static AccessKeyInformation GetTargetForElement(IInputElement? element, string key)
+        {
+            var info = new AccessKeyInformation();
+            if (element == null)
+                return info;
+
+            var args = new AccessKeyPressedEventArgs(key);
+            element.RaiseEvent(args);
+            info.Target = args.Target;
+
+            return info;
+        }
+
+        /// <summary>
+        /// Checks if the focused element is a descendent of the owner.
+        /// </summary>
+        /// <param name="owner">The owner to check.</param>
+        /// <returns>If focused element is decendant of owner <c>true</c>, otherwise <c>false</c>. </returns>
+        private static bool IsFocusWithinOwner(IInputRoot owner)
+        {
+            var focusedElement = KeyboardDevice.Instance?.FocusedElement;
+            if (focusedElement is not InputElement inputElement)
+                return false;
+
+            var isAncestorOf = owner is Visual root && root.IsVisualAncestorOf(inputElement);
+            return isAncestorOf;
+        }
+
+        /// <summary>
+        /// Sorts the list of targets according to logical ancestors in the hierarchy
+        /// so that child elements, for example within in the content of a tab,
+        /// are processed before the next parent item i.e. the next tab item.
+        /// </summary>
+        private static List<IInputElement> SortByHierarchy(List<IInputElement> targets)
+        {
+            // bail out, if there are no targets to sort
+            if (targets.Count <= 1) 
+                return targets;
+            
+            var sorted = new List<IInputElement>(targets.Count);
+            var queue = new Queue<IInputElement>(targets);
+            while (queue.Count > 0)
+            {
+                var element = queue.Dequeue();
+            
+                // if the element was already added, do nothing
+                if (sorted.Contains(element))
+                    continue;
+            
+                // add the element itself
+                sorted.Add(element);
+
+                // if the element is not a potential parent, do nothing
+                if (element is not ILogical parentElement) 
+                    continue;
+                
+                // add all descendants of the element
+                sorted.AddRange(queue
+                    .Where(child => parentElement
+                        .IsLogicalAncestorOf(child as ILogical)));
+            }
+            return sorted;
+        }
+
+        private enum ProcessKeyResult
+        {
+            NoMatch,
+            MoreMatches,
+            LastMatch
+        }
+
+        private struct AccessKeyInformation
+        {
+            public IInputElement? Target { get; set; }
+        }
+    }
+
+    /// <summary>
+    /// The inputs to an AccessKeyPressedEventHandler
+    /// </summary>
+    internal class AccessKeyPressedEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// The constructor for AccessKeyPressed event args
+        /// </summary>
+        public AccessKeyPressedEventArgs()
+        {
+            RoutedEvent = AccessKeyHandler.AccessKeyPressedEvent;
+            Key = null;
+        }
+
+        /// <summary>
+        /// Constructor for AccessKeyPressed event args
+        /// </summary>
+        /// <param name="key"></param>
+        public AccessKeyPressedEventArgs(string key) : this()
+        {
+            RoutedEvent = AccessKeyHandler.AccessKeyPressedEvent;
+            Key = key;
+        }
+
+        /// <summary>
+        /// Target element for the element that raised this event.
+        /// </summary>
+        /// <value></value>
+        public IInputElement? Target { get; set; }
+
+        /// <summary>
+        /// Key that was pressed
+        /// </summary>
+        /// <value></value>
+        public string? Key { get; }
+    }
+
+    /// <summary>
+    /// Information pertaining to when the access key associated with an element is pressed
+    /// </summary>
+    internal class AccessKeyEventArgs : RoutedEventArgs
+    {
+        /// <summary>
+        /// Constructor
+        /// </summary>
+        internal AccessKeyEventArgs(string key, bool isMultiple)
+        {
+            RoutedEvent = AccessKeyHandler.AccessKeyEvent;
+
+            Key = key;
+            IsMultiple = isMultiple;
+        }
+
+        /// <summary>
+        /// The key that was pressed which invoked this access key
+        /// </summary>
+        /// <value></value>
+        public string Key { get; }
+
+        /// <summary>
+        /// Were there other elements which are also invoked by this key
+        /// </summary>
+        /// <value></value>
+        public bool IsMultiple { get; }
+    }
+
+    internal class AccessKeyRegistration
+    {
+        private readonly WeakReference<IInputElement> _target;
+        public string Key { get; }
+
+        public AccessKeyRegistration(string key, WeakReference<IInputElement> target)
+        {
+            _target = target;
+            Key = key;
+        }
+
+        public IInputElement? GetInputElement() =>
+            _target.TryGetTarget(out var target) ? target : null;
     }
 }

+ 22 - 9
src/Avalonia.Base/Input/InputElement.cs

@@ -1,17 +1,15 @@
+#nullable enable
+
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
-using Avalonia.Data;
 using Avalonia.Input.GestureRecognizers;
 using Avalonia.Input.TextInput;
 using Avalonia.Interactivity;
 using Avalonia.Reactive;
 using Avalonia.VisualTree;
 
-#nullable enable
-
 namespace Avalonia.Input
 {
     /// <summary>
@@ -231,6 +229,10 @@ namespace Avalonia.Input
             PointerPressedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerPressed(e), handledEventsToo: true);
             PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerReleased(e), handledEventsToo: true);
             PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnGesturePointerCaptureLost(e), handledEventsToo: true);
+            
+            
+            // Access Key Handling
+            AccessKeyHandler.AccessKeyEvent.AddClassHandler<InputElement>((x, e) => x.OnAccessKey(e));
         }
 
         public InputElement()
@@ -282,7 +284,7 @@ namespace Avalonia.Input
             add { AddHandler(TextInputEvent, value); }
             remove { RemoveHandler(TextInputEvent, value); }
         }
-        
+
         /// <summary>
         /// Occurs when an input element gains input focus and input method is looking for the corresponding client
         /// </summary>
@@ -346,7 +348,7 @@ namespace Avalonia.Input
             add => AddHandler(PointerCaptureLostEvent, value);
             remove => RemoveHandler(PointerCaptureLostEvent, value);
         }
-        
+
         /// <summary>
         /// Occurs when the mouse is scrolled over the control.
         /// </summary>
@@ -355,7 +357,7 @@ namespace Avalonia.Input
             add { AddHandler(PointerWheelChangedEvent, value); }
             remove { RemoveHandler(PointerWheelChangedEvent, value); }
         }
-        
+
         /// <summary>
         /// Occurs when a tap gesture occurs on the control.
         /// </summary>
@@ -364,7 +366,7 @@ namespace Avalonia.Input
             add { AddHandler(TappedEvent, value); }
             remove { RemoveHandler(TappedEvent, value); }
         }
-        
+
         /// <summary>
         /// Occurs when a hold gesture occurs on the control.
         /// </summary>
@@ -409,7 +411,7 @@ namespace Avalonia.Input
             get { return GetValue(CursorProperty); }
             set { SetValue(CursorProperty, value); }
         }
-        
+
         /// <summary>
         /// Gets a value indicating whether keyboard focus is anywhere within the element or its visual tree child elements.
         /// </summary>
@@ -515,6 +517,17 @@ namespace Avalonia.Input
             }
         }
 
+        /// <summary>
+        /// This method is used to execute the action on an effective IInputElement when a corresponding access key has been invoked.
+        /// By default, the Focus() method is invoked with the NavigationMethod.Tab to indicate a visual focus adorner.
+        /// Overwrite this method if other methods or additional functionality is needed when an item should receive the focus.
+        /// </summary>
+        /// <param name="e">AccessKeyEventArgs are passed on to indicate if there are multiple matches or not.</param>
+        protected virtual void OnAccessKey(RoutedEventArgs e)
+        {
+            Focus(NavigationMethod.Tab);
+        }
+
         /// <inheritdoc/>
         protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
         {

+ 22 - 3
src/Avalonia.Controls/Button.cs

@@ -104,7 +104,7 @@ namespace Avalonia.Controls
         static Button()
         {
             FocusableProperty.OverrideDefaultValue(typeof(Button), true);
-            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>((lbl, args) => lbl.OnAccessKey(args));
+            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>(OnAccessKeyPressed);
         }
 
         /// <summary>
@@ -199,7 +199,7 @@ namespace Avalonia.Controls
 
         /// <inheritdoc/>
         protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
-
+        
         /// <inheritdoc/>
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
@@ -278,7 +278,18 @@ namespace Avalonia.Controls
             }
         }
 
-        protected virtual void OnAccessKey(RoutedEventArgs e) => OnClick();
+        /// <inheritdoc />
+        protected override void OnAccessKey(RoutedEventArgs e)
+        {
+            if (e is AccessKeyEventArgs { IsMultiple: true })
+            {
+                base.OnAccessKey(e);
+            }
+            else
+            {
+                OnClick();    
+            }
+        }
 
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
@@ -563,6 +574,14 @@ namespace Avalonia.Controls
 
         internal void PerformClick() => OnClick();
 
+        private static void OnAccessKeyPressed(Button sender, AccessKeyPressedEventArgs e)
+        {
+            if (e.Handled || e.Target is not null) 
+                return;
+            e.Target = sender;
+            e.Handled = true;
+        }
+        
         /// <summary>
         /// Called when the <see cref="ICommand.CanExecuteChanged"/> event fires.
         /// </summary>

+ 22 - 9
src/Avalonia.Controls/Label.cs

@@ -28,9 +28,8 @@ namespace Avalonia.Controls
 
         static Label()
         {
-            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Label>((lbl, args) => lbl.LabelActivated(args));
-            // IsTabStopProperty.OverrideDefaultValue<Label>(false)
-            FocusableProperty.OverrideDefaultValue<Label>(false);
+            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Label>(OnAccessKeyPressed);
+            IsTabStopProperty.OverrideDefaultValue<Label>(false);
         }
 
         /// <summary>
@@ -40,13 +39,11 @@ namespace Avalonia.Controls
         {
         }
 
-        /// <summary>
-        /// Method which focuses <see cref="Target"/> input element
-        /// </summary>
-        private void LabelActivated(RoutedEventArgs args)
+
+        /// <inheritdoc />
+        protected override void OnAccessKey(RoutedEventArgs e)
         {
-            Target?.Focus();
-            args.Handled = Target != null;
+            LabelActivated(e);
         }
 
         /// <summary>
@@ -62,9 +59,25 @@ namespace Avalonia.Controls
             base.OnPointerPressed(e);
         }
 
+        /// <inheritdoc />
         protected override AutomationPeer OnCreateAutomationPeer()
         {
             return new LabelAutomationPeer(this);
         }
+
+        private void LabelActivated(RoutedEventArgs e)
+        {
+            Target?.Focus();
+            e.Handled = Target != null;
+        }
+
+        private static void OnAccessKeyPressed(Label label, AccessKeyPressedEventArgs e)
+        {
+            if (e is not { Handled: false, Target: null }) 
+                return;
+            
+            e.Target = label.Target;
+            e.Handled = true;
+        }
     }
 }

+ 11 - 1
src/Avalonia.Controls/Menu.cs

@@ -40,8 +40,9 @@ namespace Avalonia.Controls
                 KeyboardNavigationMode.Once);
             AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<Menu>(AccessibilityView.Control);
             AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<Menu>(AutomationControlType.Menu);
+            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Menu>(OnAccessKeyPressed);
         }
-
+        
         /// <inheritdoc/>
         public override void Close()
         {
@@ -104,5 +105,14 @@ namespace Avalonia.Controls
             if ((element as MenuItem)?.ItemContainerTheme == ItemContainerTheme)
                 element.ClearValue(ItemContainerThemeProperty);
         }
+        
+        private static void OnAccessKeyPressed(Menu sender, AccessKeyPressedEventArgs e)
+        {
+            if (e.Handled || e.Source is not StyledElement target) 
+                return;
+            
+            e.Target = DefaultMenuInteractionHandler.GetMenuItemCore(target);
+            e.Handled = true;
+        }
     }
 }

+ 11 - 1
src/Avalonia.Controls/MenuItem.cs

@@ -143,6 +143,7 @@ namespace Avalonia.Controls
             ClickEvent.AddClassHandler<MenuItem>((x, e) => x.OnClick(e));
             SubmenuOpenedEvent.AddClassHandler<MenuItem>((x, e) => x.OnSubmenuOpened(e));
             AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue<MenuItem>(IsOffscreenBehavior.FromClip);
+            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<MenuItem>(OnAccessKeyPressed);
         }
 
         public MenuItem()
@@ -565,7 +566,7 @@ namespace Avalonia.Controls
                 }
             }
         }
-
+        
         /// <summary>
         /// Closes all submenus of the menu item.
         /// </summary>
@@ -603,6 +604,15 @@ namespace Avalonia.Controls
             }
 
         }
+        
+        private static void OnAccessKeyPressed(MenuItem sender, AccessKeyPressedEventArgs e)
+        {
+            if (e is not { Handled: false, Target: null }) 
+                return;
+            
+            e.Target = sender;
+            e.Handled = true;
+        }
 
         /// <summary>
         /// Called when the <see cref="CommandParameter"/> property changes.

+ 14 - 79
src/Avalonia.Controls/MenuItemAccessKeyHandler.cs

@@ -1,82 +1,17 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Input;
-using Avalonia.Interactivity;
 
 namespace Avalonia.Controls
 {
     /// <summary>
     /// Handles access keys within a <see cref="MenuItem"/>
     /// </summary>
-    internal class MenuItemAccessKeyHandler : IAccessKeyHandler
+    internal class MenuItemAccessKeyHandler : AccessKeyHandler
     {
-        /// <summary>
-        /// The registered access keys.
-        /// </summary>
-        private readonly List<(string AccessKey, IInputElement Element)> _registered = new();
-
-        /// <summary>
-        /// The window to which the handler belongs.
-        /// </summary>
-        private IInputRoot? _owner;
-
-        /// <summary>
-        /// Gets or sets the window's main menu.
-        /// </summary>
-        /// <remarks>
-        /// This property is ignored as a menu item cannot have a main menu.
-        /// </remarks>
-        public IMainMenu? MainMenu { get; set; }
-
-        /// <summary>
-        /// Sets the owner of the access key handler.
-        /// </summary>
-        /// <param name="owner">The owner.</param>
-        /// <remarks>
-        /// This method can only be called once, typically by the owner itself on creation.
-        /// </remarks>
-        public void SetOwner(IInputRoot owner)
-        {
-            _ = owner ?? throw new ArgumentNullException(nameof(owner));
-
-            if (_owner != null)
-            {
-                throw new InvalidOperationException("AccessKeyHandler owner has already been set.");
-            }
-
-            _owner = owner;
-
-            _owner.AddHandler(InputElement.TextInputEvent, OnTextInput);
-        }
-
-        /// <summary>
-        /// Registers an input element to be associated with an access key.
-        /// </summary>
-        /// <param name="accessKey">The access key.</param>
-        /// <param name="element">The input element.</param>
-        public void Register(char accessKey, IInputElement element)
-        {
-            var existing = _registered.FirstOrDefault(x => x.Item2 == element);
-
-            if (existing != default)
-            {
-                _registered.Remove(existing);
-            }
-
-            _registered.Add((accessKey.ToString().ToUpperInvariant(), element));
-        }
-
-        /// <summary>
-        /// Unregisters the access keys associated with the input element.
-        /// </summary>
-        /// <param name="element">The input element.</param>
-        public void Unregister(IInputElement element)
+        protected override void OnSetOwner(IInputRoot owner)
         {
-            foreach (var i in _registered.Where(x => x.Item2 == element).ToList())
-            {
-                _registered.Remove(i);
-            }
+            owner.AddHandler(InputElement.TextInputEvent, OnTextInput);
         }
 
         /// <summary>
@@ -86,17 +21,17 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         protected virtual void OnTextInput(object? sender, TextInputEventArgs e)
         {
-            if (!string.IsNullOrWhiteSpace(e.Text))
-            {
-                var text = e.Text;
-                var focus = _registered
-                    .FirstOrDefault(x => string.Equals(x.AccessKey, text, StringComparison.OrdinalIgnoreCase)
-                        && x.Element.IsEffectivelyVisible).Element;
-
-                focus?.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
-
-                e.Handled = true;
-            }
+            if (string.IsNullOrWhiteSpace(e.Text)) 
+                return;
+            
+            var key = e.Text;
+            var registration = Registrations
+                .FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase));
+                
+            if (registration == null)
+                return;
+
+            e.Handled = ProcessKey(key, registration.GetInputElement());
         }
     }
 }

+ 8 - 4
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -80,12 +80,16 @@ namespace Avalonia.Controls.Platform
         protected internal virtual void AccessKeyPressed(object? sender, RoutedEventArgs e)
         {
             var item = GetMenuItemCore(e.Source as Control);
+            if (item is null)
+                return;
 
-            if (item == null)
+            if (e is AccessKeyEventArgs { IsMultiple: true })
             {
+                // in case we have multiple matches, only focus item and bail
+                item.Focus(NavigationMethod.Tab);
                 return;
             }
-
+            
             if (item.HasSubMenu && item.IsEffectivelyEnabled)
             {
                 Open(item, true);
@@ -290,7 +294,7 @@ namespace Avalonia.Controls.Platform
             Menu.KeyDown += KeyDown;
             Menu.PointerPressed += PointerPressed;
             Menu.PointerReleased += PointerReleased;
-            Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
+            Menu.AddHandler(AccessKeyHandler.AccessKeyEvent, AccessKeyPressed);
             Menu.AddHandler(MenuBase.OpenedEvent, MenuOpened);
             Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
             Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
@@ -332,7 +336,7 @@ namespace Avalonia.Controls.Platform
             Menu.KeyDown -= KeyDown;
             Menu.PointerPressed -= PointerPressed;
             Menu.PointerReleased -= PointerReleased;
-            Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
+            Menu.RemoveHandler(AccessKeyHandler.AccessKeyEvent, AccessKeyPressed);
             Menu.RemoveHandler(MenuBase.OpenedEvent, MenuOpened);
             Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
             Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);

+ 19 - 7
src/Avalonia.Controls/TabItem.cs

@@ -40,7 +40,7 @@ namespace Avalonia.Controls
             DataContextProperty.Changed.AddClassHandler<TabItem>((x, e) => x.UpdateHeader(e));
             AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<TabItem>(AutomationControlType.TabItem);
             AutomationProperties.IsOffscreenBehaviorProperty.OverrideDefaultValue<TabItem>(IsOffscreenBehavior.FromClip);
-            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<TabItem>((tabItem, args) => tabItem.TabItemActivated(args));
+            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<TabItem>(OnAccessKeyPressed);
         }
 
         /// <summary>
@@ -61,13 +61,31 @@ namespace Avalonia.Controls
             set => SetValue(IsSelectedProperty, value);
         }
 
+        /// <inheritdoc />
+        protected override void OnAccessKey(RoutedEventArgs e)
+        {
+            Focus();
+            SetCurrentValue(IsSelectedProperty, true);
+            e.Handled = true;
+        }
+
         protected override AutomationPeer OnCreateAutomationPeer() => new ListItemAutomationPeer(this);
 
+        
         [Obsolete("Owner manages its children properties by itself")]
         protected void SubscribeToOwnerProperties(AvaloniaObject owner)
         {
         }
 
+        private static void OnAccessKeyPressed(TabItem tabItem, AccessKeyPressedEventArgs e)
+        {
+            if (e.Handled || (e.Target != null && tabItem.IsSelected)) 
+                return;
+            
+            e.Target = tabItem;
+            e.Handled = true;
+        }
+
         private void UpdateHeader(AvaloniaPropertyChangedEventArgs obj)
         {
             if (Header == null)
@@ -95,11 +113,5 @@ namespace Avalonia.Controls
                 }
             }
         }
-
-        private void TabItemActivated(RoutedEventArgs args)
-        {
-            SetCurrentValue(IsSelectedProperty, true);
-            args.Handled = true;
-        }
     }
 }

+ 75 - 48
tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs

@@ -142,68 +142,85 @@ namespace Avalonia.Base.UnitTests.Input
         }
 
         [Fact]
-        public void Should_Raise_AccessKeyPressed_For_Registered_Access_Key()
+        public void Should_Raise_AccessKey_For_Registered_Access_Key()
         {
-            var button = new Button();
-            var root = new TestRoot(button);
-            var target = new AccessKeyHandler();
-            var raised = 0;
+            using (UnitTestApplication.Start(TestServices.RealFocus))
+            {
+                var button = new Button();
+                var root = new TestRoot(button);
+                var target = new AccessKeyHandler();
+                var raised = 0;
 
-            target.SetOwner(root);
-            target.Register('A', button);
-            button.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, (s, e) => ++raised);
+                KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
 
-            KeyDown(root, Key.LeftAlt);
-            Assert.Equal(0, raised);
+                target.SetOwner(root);
+                target.Register('A', button);
+                button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised);
 
-            KeyDown(root, Key.A, KeyModifiers.Alt);
-            Assert.Equal(1, raised);
+                KeyDown(root, Key.LeftAlt);
+                Assert.Equal(0, raised);
 
-            KeyUp(root, Key.A, KeyModifiers.Alt);
-            KeyUp(root, Key.LeftAlt);
+                KeyDown(root, Key.A, KeyModifiers.Alt);
+                Assert.Equal(1, raised);
 
-            Assert.Equal(1, raised);
+                KeyUp(root, Key.A, KeyModifiers.Alt);
+                KeyUp(root, Key.LeftAlt);
+
+                Assert.Equal(1, raised);
+            }
         }
 
-        [Fact]
-        public void Should_Not_Raise_AccessKeyPressed_For_Registered_Access_Key_When_Not_Effectively_Enabled()
+        [Theory]
+        [InlineData(false, 0)]
+        [InlineData(true, 1)]
+        public void Should_Raise_AccessKey_For_Registered_Access_Key_When_Effectively_Enabled(bool enabled, int expected)
         {
-            var button = new Button();
-            var root = new TestRoot(button) { IsEnabled = false };
-            var target = new AccessKeyHandler();
-            var raised = 0;
-
-            target.SetOwner(root);
-            target.Register('A', button);
-            button.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, (s, e) => ++raised);
-
-            KeyDown(root, Key.LeftAlt);
-            Assert.Equal(0, raised);
-
-            KeyDown(root, Key.A, KeyModifiers.Alt);
-            Assert.Equal(0, raised);
-
-            KeyUp(root, Key.A, KeyModifiers.Alt);
-            KeyUp(root, Key.LeftAlt);
-
-            Assert.Equal(0, raised);
+            using (UnitTestApplication.Start(TestServices.RealFocus))
+            {
+                var button = new Button();
+                var root = new TestRoot(button) { IsEnabled = enabled };
+                var target = new AccessKeyHandler();
+                var raised = 0;
+                
+                KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None);
+                
+                target.SetOwner(root);
+                target.Register('A', button);
+                button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised);
+
+                KeyDown(root, Key.LeftAlt);
+                Assert.Equal(0, raised);
+
+                KeyDown(root, Key.A, KeyModifiers.Alt);
+                Assert.Equal(expected, raised);
+
+                KeyUp(root, Key.A, KeyModifiers.Alt);
+                KeyUp(root, Key.LeftAlt);
+                Assert.Equal(expected, raised);
+            }
         }
 
         [Fact]
         public void Should_Open_MainMenu_On_Alt_KeyUp()
         {
-            var root = new TestRoot();
-            var target = new AccessKeyHandler();
-            var menu = new Mock<IMainMenu>();
-
-            target.SetOwner(root);
-            target.MainMenu = menu.Object;
-
-            KeyDown(root, Key.LeftAlt);
-            menu.Verify(x => x.Open(), Times.Never);
-
-            KeyUp(root, Key.LeftAlt);
-            menu.Verify(x => x.Open(), Times.Once);
+            using (UnitTestApplication.Start(TestServices.RealFocus))
+            {
+                var target = new AccessKeyHandler();
+                var menu = new FakeMenu();
+                var root = new TestRoot(menu);
+
+                KeyboardDevice.Instance?.SetFocusedElement(menu, NavigationMethod.Unspecified,
+                    KeyModifiers.None);
+
+                target.SetOwner(root);
+                target.MainMenu = menu;
+
+                KeyDown(root, Key.LeftAlt);
+                Assert.Equal(0, menu.TimesOpenCalled);
+                
+                KeyUp(root, Key.LeftAlt);
+                Assert.Equal(1, menu.TimesOpenCalled);
+            }
         }
 
         private static void KeyDown(IInputElement target, Key key, KeyModifiers modifiers = KeyModifiers.None)
@@ -225,5 +242,15 @@ namespace Avalonia.Base.UnitTests.Input
                 KeyModifiers = modifiers,
             });
         }
+        
+        class FakeMenu : Menu
+        {
+            public int TimesOpenCalled { get; set; }
+            
+            public override void Open()
+            {
+                TimesOpenCalled++;
+            }
+        }
     }
 }

+ 2 - 4
tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs

@@ -1,7 +1,5 @@
-using System;
 using Avalonia.Controls;
 using Avalonia.Input;
-using Avalonia.Interactivity;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -177,10 +175,10 @@ namespace Avalonia.Base.UnitTests.Input
                 };
 
                 target.Focus();
-                target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
+                target.RaiseEvent(new AccessKeyEventArgs("b1", false));
                 Assert.False(target.IsEnabled);
                 Assert.False(target.IsFocused);
-                target1.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
+                target1.RaiseEvent(new AccessKeyEventArgs("b2", false));
                 Assert.True(target.IsEnabled);
                 Assert.False(target.IsFocused);
             }

+ 13 - 6
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -293,9 +293,9 @@ namespace Avalonia.Controls.UnitTests
             var raised = 0;
 
             target.Click += (s, e) => ++raised;
-
-            target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
-
+            
+            target.RaiseEvent(new AccessKeyEventArgs("b", false));
+            
             Assert.Equal(1, raised);
         }
 
@@ -304,7 +304,12 @@ namespace Avalonia.Controls.UnitTests
         {
             var raised = 0;
             var ah = new AccessKeyHandler();
-            using var app = UnitTestApplication.Start(TestServices.StyledWindow.With(accessKeyHandler: ah));
+            var kd = new KeyboardDevice();
+            using var app = UnitTestApplication.Start(TestServices.StyledWindow
+                .With(
+                    accessKeyHandler: ah, 
+                    keyboardDevice: () => kd)
+            );
 
             var impl = CreateMockTopLevelImpl();
             var command = new TestCommand(p => p is bool value && value, _ => raised++);
@@ -329,6 +334,8 @@ namespace Avalonia.Controls.UnitTests
                     })
                 },
             };
+            kd.SetFocusedElement(target, NavigationMethod.Unspecified, KeyModifiers.None);
+            
 
             root.ApplyTemplate();
             root.Presenter.UpdateChild();
@@ -349,7 +356,7 @@ namespace Avalonia.Controls.UnitTests
             RaiseAccessKey(root, accessKey);
 
             Assert.Equal(1, raised);
-
+            
             static FuncControlTemplate<TestTopLevel> CreateTemplate()
             {
                 return new FuncControlTemplate<TestTopLevel>((x, scope) =>
@@ -409,7 +416,7 @@ namespace Avalonia.Controls.UnitTests
             target.IsEnabled = false;
             target.Click += (s, e) => ++raised;
 
-            target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
+            target.RaiseEvent(new AccessKeyEventArgs("b", false));
 
             Assert.Equal(0, raised);
         }

+ 8 - 2
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -724,7 +724,12 @@ namespace Avalonia.Controls.UnitTests
         public void Should_TabControl_Recognizes_AccessKey(Key accessKey, int selectedTabIndex)
         {
             var ah = new AccessKeyHandler();
-            using (UnitTestApplication.Start(TestServices.StyledWindow.With(accessKeyHandler: ah)))
+            var kd = new KeyboardDevice();
+            using (UnitTestApplication.Start(TestServices.StyledWindow
+                       .With(
+                           accessKeyHandler: ah,
+                           keyboardDevice: () => kd)
+                   ))
             {
                 var impl = CreateMockTopLevelImpl();
 
@@ -742,7 +747,8 @@ namespace Avalonia.Controls.UnitTests
                         new TabItem { Header = "_Disabled", IsEnabled = false },
                     }
                 };
-
+                kd.SetFocusedElement((TabItem)tabControl.Items[selectedTabIndex], NavigationMethod.Unspecified, KeyModifiers.None);
+                
                 var root = new TestTopLevel(impl.Object)
                 {
                     Template = CreateTemplate(),

+ 1 - 1
tests/Avalonia.Markup.UnitTests/Data/BindingTests_Method.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.UnitTests.Data
                 DataContext = vm,
                 [!Button.CommandProperty] = new Binding("MyMethod"),
             };
-            target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
+            target.RaiseEvent(new AccessKeyEventArgs("b", false));
 
             Assert.False(vm.IsSet);
         }