diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index 84b348e0c2..a6ae754c43 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -516,8 +516,31 @@ if (_children) [changed addObjectsFromArray:_children]; + /* + For future reference, upon testing with a sample SwiftUI app: + + 1) Containers vanish. VStack/HStack don't appear in the accessibility tree at all, + only real elements (Text, Button, List) do, parented to one root AXHostingView. + + 2) Changes post on the nearest real element, not the container. + Toggling a child two VStack/HStack levels deep fired AXLayoutChanged on the AXHostingView + (the nearest real element), never on the hidden containers. + + 3) Real controls get their own notification. The List (AXOutline) posted AXRowCountChanged on itself. + + Apple never posts a structural notification on a non-accessibility container. + It targets the nearest element that's actually in the tree. + That's exactly what the code below does (templated parent, else walk to the nearest exposed ancestor). + */ + + id target = [AvnAccessibilityElement acquire:_peer->GetTemplatedParent()]; + if (target == nil) + target = self; + while ([target isKindOfClass:[AvnAccessibilityElement class]] && ![(AvnAccessibilityElement*)target isAccessibilityElement]) + target = [(AvnAccessibilityElement*)target accessibilityParent]; + NSAccessibilityPostNotificationWithUserInfo( - self, + target, NSAccessibilityLayoutChangedNotification, @{ NSAccessibilityUIElementsKey: [changed allObjects]}); } diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index dae105c8a4..659ba78200 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using Avalonia.Automation; using Avalonia.Automation.Peers; using Avalonia.Automation.Provider; +using Avalonia.Controls; using Avalonia.Controls.Automation.Peers; using Avalonia.Native.Interop; @@ -56,6 +57,10 @@ namespace Avalonia.Native public AvnLandmarkType LandmarkType => (AvnLandmarkType?)_inner.GetLandmarkType() ?? AvnLandmarkType.LandmarkNone; public int HeadingLevel => _inner.GetHeadingLevel(); public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); + public IAvnAutomationPeer? TemplatedParent => + _inner is ControlAutomationPeer { Owner.TemplatedParent: Control templatedParent } ? + Wrap(ControlAutomationPeer.CreatePeerForElement(templatedParent)) + : null; public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetAutomationRoot()); public AvnLiveSetting LiveSetting => (AvnLiveSetting)_inner.GetLiveSetting(); diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 3762126989..1b9ee376c6 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1274,6 +1274,7 @@ interface IAvnAutomationPeer : IUnknown IAvnAutomationPeer* GetLabeledBy(); IAvnString* GetName(); IAvnAutomationPeer* GetParent(); + IAvnAutomationPeer* GetTemplatedParent(); IAvnAutomationPeer* GetVisualRoot(); bool HasKeyboardFocus(); bool IsContentElement();