From a2a1663f4b181001f5f92e2a03a3a145551f32ac Mon Sep 17 00:00:00 2001 From: Melissa Date: Mon, 19 Jan 2026 19:19:42 +0100 Subject: [PATCH] Added automation peer for Expander control (#20475) * Added `ExpanderAutomationPeer` for `Expander` control * Use Group/"group" on UIA and NSAccessibilityDisclosureTriangleRole on AX --------- Co-authored-by: Julien Lebosquain --- native/Avalonia.Native/src/OSX/automation.mm | 1 + .../Automation/Peers/AutomationPeer.cs | 2 + .../Peers/ExpanderAutomationPeer.cs | 47 +++++++++++++++++++ src/Avalonia.Controls/Expander.cs | 7 +++ src/Avalonia.Native/avn.idl | 1 + .../AutomationNode.cs | 1 + 6 files changed, 59 insertions(+) create mode 100644 src/Avalonia.Controls/Automation/Peers/ExpanderAutomationPeer.cs diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 30e1f5ea00..f138a9386f 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -126,6 +126,7 @@ case AutomationHeaderItem: return NSAccessibilityButtonRole; case AutomationTable: return NSAccessibilityTableRole; case AutomationTitleBar: return NSAccessibilityGroupRole; + case AutomationExpander: return NSAccessibilityDisclosureTriangleRole; // Treat unknown roles as generic group container items. Returning // NSAccessibilityUnknownRole is also possible but makes the screen // reader focus on the item instead of passing focus to child items. diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs index 65f9503061..f71d57c963 100644 --- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs @@ -47,6 +47,7 @@ namespace Avalonia.Automation.Peers Table, TitleBar, Separator, + Expander, } public enum AutomationLandmarkType @@ -536,6 +537,7 @@ namespace Avalonia.Automation.Peers AutomationControlType.SplitButton => "split button", AutomationControlType.HeaderItem => "header item", AutomationControlType.TitleBar => "title bar", + AutomationControlType.Expander => "group", AutomationControlType.None => (GetLandmarkType()?.ToString() ?? controlType.ToString()).ToLowerInvariant(), _ => controlType.ToString().ToLowerInvariant(), }; diff --git a/src/Avalonia.Controls/Automation/Peers/ExpanderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ExpanderAutomationPeer.cs new file mode 100644 index 0000000000..a23e8f045a --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ExpanderAutomationPeer.cs @@ -0,0 +1,47 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; + +namespace Avalonia.Controls.Automation.Peers +{ + public class ExpanderAutomationPeer : ControlAutomationPeer, + IExpandCollapseProvider + { + public ExpanderAutomationPeer(Control owner) + : base(owner) + { + owner.PropertyChanged += OwnerPropertyChanged; + } + + public new Expander Owner => (Expander)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsExpanded); + public bool ShowsMenu => false; + public void Collapse() => Owner.IsExpanded = false; + public void Expand() => Owner.IsExpanded = true; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Expander; + } + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == Expander.IsExpandedProperty) + { + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + ToState((bool)e.OldValue!), + ToState((bool)e.NewValue!)); + } + } + + private static ExpandCollapseState ToState(bool value) + { + return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + } +} diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index aeee9f07ea..eb10516a82 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -1,6 +1,8 @@ using System; using System.Threading; using Avalonia.Animation; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -274,6 +276,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ExpanderAutomationPeer(this); + } + /// /// Updates the visual state of the control by applying latest PseudoClasses. /// diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 2fff3f3794..639f9bb71d 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -652,6 +652,7 @@ enum AvnAutomationControlType AutomationTable, AutomationTitleBar, AutomationSeparator, + AutomationExpander, } enum AvnLandmarkType diff --git a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs index fea4ca3ef3..a279fd7116 100644 --- a/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32.Automation/AutomationNode.cs @@ -357,6 +357,7 @@ namespace Avalonia.Win32.Automation AutomationControlType.Table => UiaControlTypeId.Table, AutomationControlType.TitleBar => UiaControlTypeId.TitleBar, AutomationControlType.Separator => UiaControlTypeId.Separator, + AutomationControlType.Expander => UiaControlTypeId.Group, _ => UiaControlTypeId.Custom, }; }