Browse Source

Fix macOS AXLayoutChanged notifications dropped for template-internal peers (#21595)

* Post macOS AXLayoutChanged on the changed peer's templated parent

The changed peer is usually a template-internal layout container that AppKit will not deliver notifications for; post on its templated parent (the owning control), falling back to the nearest exposed ancestor when there is none.

* Update with comment
release/12.0
Jumar Macato 4 months ago
committed by Julien Lebosquain
parent
commit
f62a0215b0
  1. 25
      native/Avalonia.Native/src/OSX/automation.mm
  2. 5
      src/Avalonia.Native/AvnAutomationPeer.cs
  3. 1
      src/Avalonia.Native/avn.idl

25
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]});
}

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

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

Loading…
Cancel
Save