diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm
index 93a67a64e0..fccb53b7aa 100644
--- a/native/Avalonia.Native/src/OSX/automation.mm
+++ b/native/Avalonia.Native/src/OSX/automation.mm
@@ -133,6 +133,38 @@
}
}
+- (NSAccessibilitySubrole)accessibilitySubrole
+{
+ auto landmarkType = _peer->GetLandmarkType();
+ switch (landmarkType) {
+ case LandmarkBanner: return @"AXLandmarkBanner";
+ case LandmarkComplementary: return @"AXLandmarkComplementary";
+ case LandmarkContentInfo: return @"AXLandmarkContentInfo";
+ case LandmarkRegion: return @"AXLandmarkRegion";
+ case LandmarkForm: return @"AXLandmarkForm";
+ case LandmarkMain: return @"AXLandmarkMain";
+ case LandmarkNavigation: return @"AXLandmarkNavigation";
+ case LandmarkSearch: return @"AXLandmarkSearch";
+ default: return NSAccessibilityUnknownSubrole;
+ }
+}
+
+- (NSString *)accessibilityRoleDescription
+{
+ auto landmarkType = _peer->GetLandmarkType();
+ switch (landmarkType) {
+ case LandmarkBanner: return @"banner";
+ case LandmarkComplementary: return @"complementary";
+ case LandmarkContentInfo: return @"content";
+ case LandmarkRegion: return @"region";
+ case LandmarkForm: return @"form";
+ case LandmarkMain: return @"main";
+ case LandmarkNavigation: return @"navigation";
+ case LandmarkSearch: return @"search";
+ }
+ return NSAccessibilityRoleDescription([self accessibilityRole], [self accessibilitySubrole]);
+}
+
- (NSString *)accessibilityIdentifier
{
return GetNSStringAndRelease(_peer->GetAutomationId());
diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index effed7964a..a03f6ea30a 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -37,14 +37,17 @@
DisplayMemberBinding="{Binding Name}"
ItemsSource="{Binding Pages}"
SelectedItem="{Binding SelectedPage}"
- SelectionChanged="Pager_SelectionChanged">
+ SelectionChanged="Pager_SelectionChanged"
+ AutomationProperties.LandmarkType="Navigation">
-
+
diff --git a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs
index 27d7bedcb9..8cdee8e3fe 100644
--- a/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs
+++ b/src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs
@@ -32,7 +32,13 @@ namespace Avalonia.Automation
public static AutomationProperty HelpTextProperty { get; } = new AutomationProperty();
///
- /// Identifiers the heading level automation property. The class name property value is returned
+ /// Identifies the landmark type automation property. The class name property value is returned
+ /// by the method.
+ ///
+ public static AutomationProperty LandmarkTypeProperty { get; } = new AutomationProperty();
+
+ ///
+ /// Identifies the heading level automation property. The class name property value is returned
/// by the method.
///
public static AutomationProperty HeadingLevelProperty { get; } = new AutomationProperty();
diff --git a/src/Avalonia.Controls/Automation/AutomationProperties.cs b/src/Avalonia.Controls/Automation/AutomationProperties.cs
index 989aa28935..4e11715c56 100644
--- a/src/Avalonia.Controls/Automation/AutomationProperties.cs
+++ b/src/Avalonia.Controls/Automation/AutomationProperties.cs
@@ -104,6 +104,17 @@ namespace Avalonia.Automation
"HelpText",
typeof(AutomationProperties));
+ ///
+ /// Defines the AutomationProperties.LandmarkType attached property.
+ ///
+ ///
+ /// This property affects the default value for
+ ///
+ public static readonly AttachedProperty LandmarkTypeProperty =
+ AvaloniaProperty.RegisterAttached(
+ "LandmarkType",
+ typeof(AutomationProperties));
+
///
/// Defines the AutomationProperties.HeadingLevel attached property.
///
@@ -359,6 +370,24 @@ namespace Avalonia.Automation
return element.GetValue(HelpTextProperty);
}
+ ///
+ /// Helper for setting the value of the on a StyledElement.
+ ///
+ public static void SetLandmarkType(StyledElement element, AutomationLandmarkType? value)
+ {
+ _ = element ?? throw new ArgumentNullException(nameof(element));
+ element.SetValue(LandmarkTypeProperty, value);
+ }
+
+ ///
+ /// Helper for reading the value of the on a StyledElement.
+ ///
+ public static AutomationLandmarkType? GetLandmarkType(StyledElement element)
+ {
+ _ = element ?? throw new ArgumentNullException(nameof(element));
+ return element.GetValue(LandmarkTypeProperty);
+ }
+
///
/// Helper for setting the value of the on a StyledElement.
///
@@ -371,7 +400,6 @@ namespace Avalonia.Automation
///
/// Helper for reading the value of the on a StyledElement.
///
- ///
public static int GetHeadingLevel(StyledElement element)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
index d46a24cd59..65f9503061 100644
--- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
@@ -49,6 +49,18 @@ namespace Avalonia.Automation.Peers
Separator,
}
+ public enum AutomationLandmarkType
+ {
+ Banner,
+ Complementary,
+ ContentInfo,
+ Region,
+ Form,
+ Main,
+ Navigation,
+ Search,
+ }
+
///
/// Provides a base class that exposes an element to UI Automation.
///
@@ -270,6 +282,25 @@ namespace Avalonia.Automation.Peers
///
public string GetHelpText() => GetHelpTextCore() ?? string.Empty;
+ ///
+ /// Gets the control type for the element that is associated with the UI Automation peer.
+ ///
+ ///
+ /// Gets the type of the element.
+ ///
+ ///
+ /// -
+ /// Windows
+ /// UIA_LandmarkTypePropertyId, UIA_LocalizedLandmarkTypePropertyId
+ ///
+ /// -
+ /// macOS
+ /// NSAccessibilityProtocol.accessibilityRole, NSAccessibilityProtocol.accessibilitySubrole
+ ///
+ ///
+ ///
+ public AutomationLandmarkType? GetLandmarkType() => GetLandmarkTypeCore();
+
///
/// Gets the heading level that is associated with this automation peer.
///
@@ -505,6 +536,7 @@ namespace Avalonia.Automation.Peers
AutomationControlType.SplitButton => "split button",
AutomationControlType.HeaderItem => "header item",
AutomationControlType.TitleBar => "title bar",
+ AutomationControlType.None => (GetLandmarkType()?.ToString() ?? controlType.ToString()).ToLowerInvariant(),
_ => controlType.ToString().ToLowerInvariant(),
};
}
@@ -520,6 +552,7 @@ namespace Avalonia.Automation.Peers
protected abstract AutomationPeer? GetLabeledByCore();
protected abstract string? GetNameCore();
protected virtual string? GetHelpTextCore() => null;
+ protected virtual AutomationLandmarkType? GetLandmarkTypeCore() => null;
protected virtual int GetHeadingLevelCore() => 0;
protected abstract AutomationPeer? GetParentCore();
protected abstract bool HasKeyboardFocusCore();
diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
index 72ad641963..ef14be0169 100644
--- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
@@ -134,6 +134,7 @@ namespace Avalonia.Automation.Peers
return result;
}
+ protected override AutomationLandmarkType? GetLandmarkTypeCore() => AutomationProperties.GetLandmarkType(Owner);
protected override int GetHeadingLevelCore() => AutomationProperties.GetHeadingLevel(Owner);
protected override AutomationPeer? GetParentCore()
{
diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs
index df268100e9..c666ffc695 100644
--- a/src/Avalonia.Native/AvnAutomationPeer.cs
+++ b/src/Avalonia.Native/AvnAutomationPeer.cs
@@ -40,6 +40,7 @@ namespace Avalonia.Native
public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy());
public IAvnString Name => _inner.GetName().ToAvnString();
public IAvnString HelpText => _inner.GetHelpText().ToAvnString();
+ public AvnLandmarkType LandmarkType => (AvnLandmarkType?)_inner.GetLandmarkType() ?? AvnLandmarkType.LandmarkNone;
public int HeadingLevel => _inner.GetHeadingLevel();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl
index 5e68635511..b80da03bd7 100644
--- a/src/Avalonia.Native/avn.idl
+++ b/src/Avalonia.Native/avn.idl
@@ -654,6 +654,19 @@ enum AvnAutomationControlType
AutomationSeparator,
}
+enum AvnLandmarkType
+{
+ LandmarkNone = -1,
+ LandmarkBanner,
+ LandmarkComplementary,
+ LandmarkContentInfo,
+ LandmarkRegion,
+ LandmarkForm,
+ LandmarkMain,
+ LandmarkNavigation,
+ LandmarkSearch,
+}
+
enum AvnWindowTransparencyMode
{
Opaque,
@@ -1307,6 +1320,8 @@ interface IAvnAutomationPeer : IUnknown
void ValueProvider_SetValue(char* value);
IAvnString* GetHelpText();
+
+ AvnLandmarkType GetLandmarkType();
int GetHeadingLevel();
}
diff --git a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs
index 611507cba5..45216a4c09 100644
--- a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs
+++ b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs
@@ -37,6 +37,7 @@ namespace Avalonia.Win32.Automation
{ AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName },
{ AutomationElementIdentifiers.NameProperty, UiaPropertyId.Name },
{ AutomationElementIdentifiers.HelpTextProperty, UiaPropertyId.HelpText },
+ { AutomationElementIdentifiers.LandmarkTypeProperty, UiaPropertyId.LandmarkType },
{ AutomationElementIdentifiers.HeadingLevelProperty, UiaPropertyId.HeadingLevel },
{ ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState },
{ RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly},
@@ -139,6 +140,8 @@ namespace Avalonia.Win32.Automation
UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()),
UiaPropertyId.Name => InvokeSync(() => Peer.GetName()),
UiaPropertyId.HelpText => InvokeSync(() => Peer.GetHelpText()),
+ UiaPropertyId.LandmarkType => InvokeSync(() => ToUiaLandmarkType(Peer.GetLandmarkType())),
+ UiaPropertyId.LocalizedLandmarkType => InvokeSync(() => ToUiaLocalizedLandmarkType(Peer.GetLandmarkType())),
UiaPropertyId.HeadingLevel => InvokeSync(() => ToUiaHeadingLevel(Peer.GetHeadingLevel())),
UiaPropertyId.ProcessId => s_pid,
UiaPropertyId.RuntimeId => _runtimeId,
@@ -360,6 +363,34 @@ namespace Avalonia.Win32.Automation
};
}
+ private static UiaLandmarkType? ToUiaLandmarkType(AutomationLandmarkType? landmarkType)
+ {
+ return landmarkType switch
+ {
+ AutomationLandmarkType.Banner or
+ AutomationLandmarkType.Complementary or
+ AutomationLandmarkType.ContentInfo or
+ AutomationLandmarkType.Region => UiaLandmarkType.Custom,
+ AutomationLandmarkType.Form => UiaLandmarkType.Form,
+ AutomationLandmarkType.Main => UiaLandmarkType.Main,
+ AutomationLandmarkType.Navigation => UiaLandmarkType.Navigation,
+ AutomationLandmarkType.Search => UiaLandmarkType.Search,
+ _ => null,
+ };
+ }
+
+ private static string? ToUiaLocalizedLandmarkType(AutomationLandmarkType? landmarkType)
+ {
+ return landmarkType switch
+ {
+ AutomationLandmarkType.Banner => "banner",
+ AutomationLandmarkType.Complementary => "complementary",
+ AutomationLandmarkType.ContentInfo => "content information",
+ AutomationLandmarkType.Region => "region",
+ _ => null,
+ };
+ }
+
private static UiaHeadingLevel ToUiaHeadingLevel(int level)
{
return level switch
diff --git a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs
index 71a5fc37a6..bbbb2a29e2 100644
--- a/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs
+++ b/src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs
@@ -277,6 +277,15 @@ internal enum UiaControlTypeId
AppBar
};
+internal enum UiaLandmarkType
+{
+ Custom = 80000,
+ Form,
+ Main,
+ Navigation,
+ Search,
+};
+
internal enum UiaHeadingLevel
{
None = 80050,