diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index 0ccca5d4cc..c72d45783c 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -5,56 +5,64 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.MainWindow"
Title="IntegrationTestApp">
-
-
-
- TextBlockWithName
-
- TextBlockWithNameAndAutomationId
-
- Label for TextBox
-
- Foo
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ TextBlockWithName
+
+ TextBlockWithNameAndAutomationId
+
+ Label for TextBox
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+
-
-
- Unchecked
- Checked
- ThreeState
-
-
+
+
+ Unchecked
+ Checked
+ ThreeState
+
+
-
-
-
- Foo
- Bar
-
-
- Foo
- Bar
-
-
- Foo
- Bar
-
-
-
-
+
+
+
+ Foo
+ Bar
+
+
+ Foo
+ Bar
+
+
+ Foo
+ Bar
+
+
+
+
+
diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
index 8a30661359..4c517fcd71 100644
--- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Globalization;
using Avalonia.Automation.Platform;
@@ -75,6 +76,17 @@ namespace Avalonia.Automation.Peers
///
public void BringIntoView() => BringIntoViewCore();
+ ///
+ /// Gets the accelerator key combinations for the element that is associated with the UI
+ /// Automation peer.
+ ///
+ public string? GetAcceleratorKey() => GetAcceleratorKeyCore();
+
+ ///
+ /// Gets the access key for the element that is associated with the automation peer.
+ ///
+ public string? GetAccessKey() => GetAccessKeyCore();
+
///
/// Gets the control type for the element that is associated with the UI Automation peer.
///
@@ -208,6 +220,8 @@ namespace Avalonia.Automation.Peers
}
protected abstract void BringIntoViewCore();
+ protected abstract string? GetAcceleratorKeyCore();
+ protected abstract string? GetAccessKeyCore();
protected abstract AutomationControlType GetAutomationControlTypeCore();
protected abstract string? GetAutomationIdCore();
protected abstract Rect GetBoundingRectangleCore();
diff --git a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs
index 89c80e1144..66e5bd7b49 100644
--- a/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs
@@ -13,13 +13,27 @@ namespace Avalonia.Automation.Peers
: base(factory, owner)
{
}
-
+
+ public new Button Owner => (Button)base.Owner;
+
public void Invoke()
{
EnsureEnabled();
(Owner as Button)?.PerformClick();
}
+ protected override string? GetAcceleratorKeyCore()
+ {
+ var result = base.GetAcceleratorKeyCore();
+
+ if (string.IsNullOrWhiteSpace(result))
+ {
+ result = Owner.HotKey?.ToString();
+ }
+
+ return result;
+ }
+
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Button;
diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
index 721272c83e..d11946b27e 100644
--- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
@@ -92,6 +92,8 @@ namespace Avalonia.Automation.Peers
}
}
+ protected override string? GetAcceleratorKeyCore() => null;
+ protected override string? GetAccessKeyCore() => null;
protected override string? GetAutomationIdCore() => null;
protected override string GetClassNameCore() => typeof(ComboBoxItem).Name;
protected override AutomationPeer? GetLabeledByCore() => null;
diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
index e68bf2ea47..cba5b4d79c 100644
--- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
@@ -148,6 +148,8 @@ namespace Avalonia.Automation.Peers
return true;
}
+ protected override string? GetAcceleratorKeyCore() => AutomationProperties.GetAcceleratorKey(Owner);
+ protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner);
protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name;
protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds);
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom;
diff --git a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs
index b1f7774fc9..b9f032ef0d 100644
--- a/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs
@@ -1,5 +1,6 @@
using Avalonia.Automation.Platform;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
#nullable enable
@@ -14,6 +15,33 @@ namespace Avalonia.Automation.Peers
public new MenuItem Owner => (MenuItem)base.Owner;
+ protected override string? GetAccessKeyCore()
+ {
+ var result = base.GetAccessKeyCore();
+
+ if (string.IsNullOrWhiteSpace(result))
+ {
+ if (Owner.HeaderPresenter.Child is AccessText accessText)
+ {
+ result = accessText.AccessKey.ToString();
+ }
+ }
+
+ return result;
+ }
+
+ protected override string? GetAcceleratorKeyCore()
+ {
+ var result = base.GetAcceleratorKeyCore();
+
+ if (string.IsNullOrWhiteSpace(result))
+ {
+ result = Owner.InputGesture?.ToString();
+ }
+
+ return result;
+ }
+
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.MenuItem;
@@ -23,14 +51,9 @@ namespace Avalonia.Automation.Peers
{
var result = base.GetNameCore();
- if (result is null && Owner.HeaderPresenter.Child is TextBlock text)
- {
- result = text.Text;
- }
-
- if (result is null)
+ if (result is null && Owner.Header is string header)
{
- result = Owner.Header?.ToString();
+ result = AccessText.RemoveAccessKeyMarker(header);
}
return result;
diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs
index 7a5e6ce426..061fd359f1 100644
--- a/src/Avalonia.Controls/Primitives/AccessText.cs
+++ b/src/Avalonia.Controls/Primitives/AccessText.cs
@@ -1,4 +1,6 @@
using System;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@@ -76,6 +78,37 @@ namespace Avalonia.Controls.Primitives
}
}
+ internal static string RemoveAccessKeyMarker(string text)
+ {
+ if (!string.IsNullOrEmpty(text))
+ {
+ var accessKeyMarker = "_";
+ var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker;
+ int index = FindAccessKeyMarker(text);
+ if (index >= 0 && index < text.Length - 1)
+ text = text.Remove(index, 1);
+ text = text.Replace(doubleAccessKeyMarker, accessKeyMarker);
+ }
+ return text;
+ }
+
+ private static int FindAccessKeyMarker(string text)
+ {
+ var length = text.Length;
+ var startIndex = 0;
+ while (startIndex < length)
+ {
+ int index = text.IndexOf('_', startIndex);
+ if (index == -1)
+ return -1;
+ if (index + 1 < length && text[index + 1] != '_')
+ return index;
+ startIndex = index + 2;
+ }
+
+ return -1;
+ }
+
///
/// Get the pixel location relative to the top-left of the layout box given the text position.
///
@@ -180,6 +213,11 @@ namespace Avalonia.Controls.Primitives
}
}
+ protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+ {
+ return new NoneAutomationPeer(factory, this);
+ }
+
///
/// Returns a string with the first underscore stripped.
///
diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
index 3757958cca..8a5488b1f1 100644
--- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
+++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
@@ -116,6 +116,8 @@ namespace Avalonia.Win32.Automation
{
return (UiaPropertyId)propertyId switch
{
+ UiaPropertyId.AcceleratorKey => InvokeSync(() => Peer.GetAcceleratorKey()),
+ UiaPropertyId.AccessKey => InvokeSync(() => Peer.GetAccessKey()),
UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()),
UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()),
UiaPropertyId.ClickablePoint => new[] { BoundingRectangle.Center.X, BoundingRectangle.Center.Y },
diff --git a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs
index b0d300e9fe..21c2b1a7e3 100644
--- a/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs
+++ b/tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs
@@ -42,5 +42,13 @@ namespace Avalonia.IntegrationTests.Win32
Assert.Equal("Button with TextBlock", button.Text);
}
+
+ [Fact]
+ public void ButtonWithAcceleratorKey()
+ {
+ var button = _session.FindElementByAccessibilityId("ButtonWithAcceleratorKey");
+
+ Assert.Equal("Ctrl+B", button.GetAttribute("AcceleratorKey"));
+ }
}
}
diff --git a/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs b/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs
new file mode 100644
index 0000000000..3d93afec12
--- /dev/null
+++ b/tests/Avalonia.IntegrationTests.Win32/MenuTests.cs
@@ -0,0 +1,31 @@
+using OpenQA.Selenium.Appium.Windows;
+using Xunit;
+
+namespace Avalonia.IntegrationTests.Win32
+{
+ [Collection("Default")]
+ public class MenuTests
+ {
+ private WindowsDriver _session;
+
+ public MenuTests(TestAppFixture fixture) => _session = fixture.Session;
+
+ [Fact]
+ public void File()
+ {
+ var fileMenu = _session.FindElementByAccessibilityId("FileMenu");
+
+ Assert.Equal("File", fileMenu.Text);
+ }
+
+ [Fact]
+ public void Open()
+ {
+ var fileMenu = _session.FindElementByAccessibilityId("FileMenu");
+ fileMenu.Click();
+
+ var openMenu = fileMenu.FindElementByAccessibilityId("OpenMenu");
+ Assert.Equal("Ctrl+O", openMenu.GetAttribute("AcceleratorKey"));
+ }
+ }
+}