Sfoglia il codice sorgente

Added HeadingLevel to AutomationProperties (#19696)

* Added HeadingLevel to AutomationProperties

* Added support for HeadlingLevel accessibility on Mac
Melissa 3 settimane fa
parent
commit
a8f3628da9

+ 5 - 1
native/Avalonia.Native/src/OSX/automation.mm

@@ -122,7 +122,7 @@
         case AutomationSplitButton: return NSAccessibilityPopUpButtonRole;
         case AutomationWindow: return NSAccessibilityWindowRole;
         case AutomationPane: return NSAccessibilityGroupRole;
-        case AutomationHeader: return NSAccessibilityGroupRole;
+        case AutomationHeader: return @"AXHeading";
         case AutomationHeaderItem:  return NSAccessibilityButtonRole;
         case AutomationTable: return NSAccessibilityTableRole;
         case AutomationTitleBar: return NSAccessibilityGroupRole;
@@ -176,6 +176,10 @@
     {
         return GetNSStringAndRelease(_peer->GetName());
     }
+    else if (_peer->GetAutomationControlType() == AutomationHeader)
+    {
+        return [NSNumber numberWithInt:_peer->GetHeadingLevel()];
+    }
 
     return [super accessibilityValue];
 }

+ 9 - 0
samples/IntegrationTestApp/Pages/AutomationPage.axaml

@@ -13,5 +13,14 @@
     <TextBox Name="LabeledByTextBox" AutomationProperties.LabeledBy="{Binding #TextBlockAsLabel}">
       Foo
     </TextBox>
+    <TextBlock Name="TextBlockWithHeader1" AutomationProperties.ControlTypeOverride="Header" AutomationProperties.HeadingLevel="1">
+      Header 1
+    </TextBlock>
+    <TextBlock Name="TextBlockWithHeader2" AutomationProperties.ControlTypeOverride="Header" AutomationProperties.HeadingLevel="2">
+      Header 2
+    </TextBlock>
+    <TextBlock Name="TextBlockWithoutHeader">
+      Header None
+    </TextBlock>
   </StackPanel>
 </UserControl>

+ 6 - 0
src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs

@@ -30,5 +30,11 @@ namespace Avalonia.Automation
         /// by the <see cref="AutomationPeer.GetHelpText"/> method.
         /// </summary>
         public static AutomationProperty HelpTextProperty { get; } = new AutomationProperty();
+
+        /// <summary>
+        /// Identifiers 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 - 0
src/Avalonia.Controls/Automation/AutomationProperties.cs

@@ -104,6 +104,17 @@ namespace Avalonia.Automation
                 "HelpText",
                 typeof(AutomationProperties));
 
+        /// <summary>
+        /// Defines the AutomationProperties.HeadingLevel attached property.
+        /// </summary>
+        /// <remarks>
+        /// This property affects the default value for <see cref="AutomationPeer.GetHeadingLevel"/>.
+        /// </remarks>
+        public static readonly AttachedProperty<int> HeadingLevelProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, int>(
+                "HeadingLevel",
+                typeof(AutomationProperties));
+
         /// <summary>
         /// Defines the AutomationProperties.IsColumnHeader attached property.
         /// </summary>
@@ -348,6 +359,25 @@ namespace Avalonia.Automation
             return element.GetValue(HelpTextProperty);
         }
 
+        /// <summary>
+        /// Helper for setting the value of the <see cref="HeadingLevelProperty"/> on a StyledElement.
+        /// </summary>
+        public static void SetHeadingLevel(StyledElement element, int value)
+        {
+            _ = element ?? throw new ArgumentNullException(nameof(element));
+            element.SetValue(HeadingLevelProperty, value);
+        }
+
+        /// <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));
+            return element.GetValue(HeadingLevelProperty);
+        }
+
         /// <summary>
         /// Helper for setting the value of the <see cref="IsColumnHeaderProperty"/> on a StyledElement. 
         /// </summary>

+ 18 - 0
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@@ -270,6 +270,23 @@ namespace Avalonia.Automation.Peers
         /// </remarks>
         public string GetHelpText() => GetHelpTextCore() ?? string.Empty;
 
+        /// <summary>
+        /// Gets the heading level that is associated with this automation peer.
+        /// </summary>
+        /// <remarks>
+        /// <list type="table">
+        ///   <item>
+        ///     <term>Windows</term>
+        ///     <description><c>UIA_HeadingLevelPropertyId</c></description>
+        ///   </item>
+        ///   <item>
+        ///     <term>macOS</term>
+        ///     <description><c>NSAccessibilityProtocol.accessibilityValue</c></description>
+        ///   </item>
+        /// </list>
+        /// </remarks>
+        public int GetHeadingLevel() => GetHeadingLevelCore();
+
         /// <summary>
         /// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
         /// </summary>
@@ -503,6 +520,7 @@ namespace Avalonia.Automation.Peers
         protected abstract AutomationPeer? GetLabeledByCore();
         protected abstract string? GetNameCore();
         protected virtual string? GetHelpTextCore() => null;
+        protected virtual int GetHeadingLevelCore() => 0;
         protected abstract AutomationPeer? GetParentCore();
         protected abstract bool HasKeyboardFocusCore();
         protected abstract bool IsContentElementCore();

+ 1 - 0
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@@ -134,6 +134,7 @@ namespace Avalonia.Automation.Peers
 
             return result;
         }
+        protected override int GetHeadingLevelCore() => AutomationProperties.GetHeadingLevel(Owner);
         protected override AutomationPeer? GetParentCore()
         {
             EnsureConnected();

+ 1 - 0
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 int HeadingLevel => _inner.GetHeadingLevel();
         public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
         public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
 

+ 1 - 0
src/Avalonia.Native/avn.idl

@@ -1238,6 +1238,7 @@ interface IAvnAutomationPeer : IUnknown
      void ValueProvider_SetValue(char* value);
 
      IAvnString* GetHelpText();
+     int GetHeadingLevel();
 }
 
 [uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)]

+ 19 - 0
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.HeadingLevelProperty, UiaPropertyId.HeadingLevel },
             { ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState },
             { RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly},
             { RangeValuePatternIdentifiers.MaximumProperty, UiaPropertyId.RangeValueMaximum },
@@ -138,6 +139,7 @@ namespace Avalonia.Win32.Automation
                 UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()),
                 UiaPropertyId.Name => InvokeSync(() => Peer.GetName()),
                 UiaPropertyId.HelpText => InvokeSync(() => Peer.GetHelpText()),
+                UiaPropertyId.HeadingLevel => InvokeSync(() => ToUiaHeadingLevel(Peer.GetHeadingLevel())),
                 UiaPropertyId.ProcessId => s_pid,
                 UiaPropertyId.RuntimeId => _runtimeId,
                 _ => null,
@@ -358,6 +360,23 @@ namespace Avalonia.Win32.Automation
             };
         }
 
+        private static UiaHeadingLevel ToUiaHeadingLevel(int level)
+        {
+            return level switch
+            {
+                1 => UiaHeadingLevel.Level1,
+                2 => UiaHeadingLevel.Level2,
+                3 => UiaHeadingLevel.Level3,
+                4 => UiaHeadingLevel.Level4,
+                5 => UiaHeadingLevel.Level5,
+                6 => UiaHeadingLevel.Level6,
+                7 => UiaHeadingLevel.Level7,
+                8 => UiaHeadingLevel.Level8,
+                9 => UiaHeadingLevel.Level9,
+                _ => UiaHeadingLevel.None,
+            };
+        }
+
         private static int GetProcessId()
         {
 #if NET6_0_OR_GREATER

+ 22 - 1
src/Windows/Avalonia.Win32.Automation/Interop/IRawElementProviderSimple.cs

@@ -184,7 +184,14 @@ internal enum UiaPropertyId
     OutlineThickness,
     CenterPoint,
     Rotatation,
-    Size
+    Size,
+    IsSelectionPattern2Available,
+    Selection2FirstSelectedItem,
+    Selection2LastSelectedItem,
+    Selection2CurrentSelectedItem,
+    Selection2ItemCount,
+    HeadingLevel,
+    IsDialog
 }
 
 internal enum UiaPatternId
@@ -270,6 +277,20 @@ internal enum UiaControlTypeId
     AppBar
 };
 
+internal enum UiaHeadingLevel
+{
+    None = 80050,
+    Level1,
+    Level2,
+    Level3,
+    Level4,
+    Level5,
+    Level6,
+    Level7,
+    Level8,
+    Level9
+};
+
 #if NET8_0_OR_GREATER
 [GeneratedComInterface]
 #else