Browse Source

Added LandmarkType to automation properties (#20134)

* Added LandmarkType to automation properties

* Set AutomationProperties.AccessibilityView on Main landmark in IntegrationTestApp, which is required for Narrator to find the landmark

* Implement AXRoleDescription as suggested by W3C

* Fixed wrong role descriptions
pull/20156/head
Melissa 2 months ago
committed by GitHub
parent
commit
d2817201cb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 32
      native/Avalonia.Native/src/OSX/automation.mm
  2. 7
      samples/IntegrationTestApp/MainWindow.axaml
  3. 8
      src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs
  4. 30
      src/Avalonia.Controls/Automation/AutomationProperties.cs
  5. 33
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  6. 1
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  7. 1
      src/Avalonia.Native/AvnAutomationPeer.cs
  8. 15
      src/Avalonia.Native/avn.idl
  9. 31
      src/Windows/Avalonia.Win32.Automation/AutomationNode.cs
  10. 9
      src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs

32
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());

7
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">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<Decorator Name="PagerContent"/>
<Decorator Name="PagerContent"
AutomationProperties.AccessibilityView="Control"
AutomationProperties.LandmarkType="Main"/>
</DockPanel>
</DockPanel>

8
src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs

@ -32,7 +32,13 @@ namespace Avalonia.Automation
public static AutomationProperty HelpTextProperty { get; } = new AutomationProperty();
/// <summary>
/// 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 <see cref="AutomationPeer.GetLandmarkType"/> method.
/// </summary>
public static AutomationProperty LandmarkTypeProperty { get; } = new AutomationProperty();
/// <summary>
/// Identifies the heading level automation property. The class name property value is returned
/// by the <see cref="AutomationPeer.GetHeadingLevel"/> method.
/// </summary>
public static AutomationProperty HeadingLevelProperty { get; } = new AutomationProperty();

30
src/Avalonia.Controls/Automation/AutomationProperties.cs

@ -104,6 +104,17 @@ namespace Avalonia.Automation
"HelpText",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.LandmarkType attached property.
/// </summary>
/// <remarks>
/// This property affects the default value for <see cref="AutomationPeer.GetLandmarkType"/>
/// </remarks>
public static readonly AttachedProperty<AutomationLandmarkType?> LandmarkTypeProperty =
AvaloniaProperty.RegisterAttached<StyledElement, AutomationLandmarkType?>(
"LandmarkType",
typeof(AutomationProperties));
/// <summary>
/// Defines the AutomationProperties.HeadingLevel attached property.
/// </summary>
@ -359,6 +370,24 @@ namespace Avalonia.Automation
return element.GetValue(HelpTextProperty);
}
/// <summary>
/// Helper for setting the value of the <see cref="LandmarkTypeProperty"/> on a StyledElement.
/// </summary>
public static void SetLandmarkType(StyledElement element, AutomationLandmarkType? value)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
element.SetValue(LandmarkTypeProperty, value);
}
/// <summary>
/// Helper for reading the value of the <see cref="LandmarkTypeProperty"/> on a StyledElement.
/// </summary>
public static AutomationLandmarkType? GetLandmarkType(StyledElement element)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
return element.GetValue(LandmarkTypeProperty);
}
/// <summary>
/// Helper for setting the value of the <see cref="HeadingLevelProperty"/> on a StyledElement.
/// </summary>
@ -371,7 +400,6 @@ namespace Avalonia.Automation
/// <summary>
/// Helper for reading the value of the <see cref="HeadingLevelProperty"/> on a StyledElement.
/// </summary>
/// <returns></returns>
public static int GetHeadingLevel(StyledElement element)
{
_ = element ?? throw new ArgumentNullException(nameof(element));

33
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,
}
/// <summary>
/// Provides a base class that exposes an element to UI Automation.
/// </summary>
@ -270,6 +282,25 @@ namespace Avalonia.Automation.Peers
/// </remarks>
public string GetHelpText() => GetHelpTextCore() ?? string.Empty;
/// <summary>
/// Gets the control type for the element that is associated with the UI Automation peer.
/// </summary>
/// <remarks>
/// Gets the type of the element.
///
/// <list type="table">
/// <item>
/// <term>Windows</term>
/// <description><c>UIA_LandmarkTypePropertyId</c>, <c>UIA_LocalizedLandmarkTypePropertyId</c></description>
/// </item>
/// <item>
/// <term>macOS</term>
/// <description><c>NSAccessibilityProtocol.accessibilityRole</c>, <c>NSAccessibilityProtocol.accessibilitySubrole</c></description>
/// </item>
/// </list>
/// </remarks>
public AutomationLandmarkType? GetLandmarkType() => GetLandmarkTypeCore();
/// <summary>
/// Gets the heading level that is associated with this automation peer.
/// </summary>
@ -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();

1
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()
{

1
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());

15
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();
}

31
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

9
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,

Loading…
Cancel
Save