diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs
index cf9a050185d..1d3749692ba 100644
--- a/src/Avalonia.Base/Visual.cs
+++ b/src/Avalonia.Base/Visual.cs
@@ -548,7 +548,7 @@ protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArg
}
DisableTransitions();
- UpdateIsEffectivelyVisible(true);
+ UpdateIsEffectivelyVisible(false);
OnDetachedFromVisualTree(e);
DetachFromCompositor();
diff --git a/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs
index 128c1e1dcc6..3c5fc77734a 100644
--- a/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs
+++ b/src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs
@@ -6,7 +6,7 @@ namespace Avalonia.Automation
public enum IsOffscreenBehavior
{
///
- /// The AutomationProperty IsOffscreen is calculated based on IsVisible.
+ /// The AutomationProperty IsOffscreen is calculated based on IsEffectivelyVisible.
///
Default,
///
diff --git a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs
index df24222a0c5..40c6cd31344 100644
--- a/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs
@@ -15,19 +15,20 @@ protected ContentControlAutomationPeer(ContentControl owner)
protected override string? GetNameCore()
{
- var result = base.GetNameCore();
-
- if (result is null && Owner.Presenter?.Child is TextBlock text)
- {
- result = text.Text;
- }
-
- if (result is null)
- {
- result = Owner.Content?.ToString();
- }
+ Control? childControl = Owner.Presenter?.Child;
+ AutomationPeer? childPeer = childControl is null ? null :
+ CreatePeerForElement(childControl);
+ return base.GetNameCore() ?? (childControl as TextBlock)?.Text ??
+ childPeer?.GetName() ?? Owner.Content?.ToString();
+ }
- return result;
+ protected override string? GetHelpTextCore()
+ {
+ Control? childControl = Owner.Presenter?.Child;
+ AutomationPeer? childPeer = childControl is null ? null :
+ CreatePeerForElement(childControl);
+ return base.GetHelpTextCore() ??
+ childPeer?.GetHelpText();
}
protected override bool IsContentElementCore() => false;
diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
index 0dff2db3c2f..9a2aa560665 100644
--- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
@@ -221,7 +221,7 @@ protected override bool IsOffscreenCore()
IsOffscreenBehavior.FromClip => Owner.GetTransformedBounds() is not { } bounds ||
MathUtilities.IsZero(bounds.Clip.Width) ||
MathUtilities.IsZero(bounds.Clip.Height),
- _ => !Owner.IsVisible,
+ _ => !Owner.IsEffectivelyVisible,
};
}
diff --git a/src/iOS/Avalonia.iOS/AutomationPeerWrapper.cs b/src/iOS/Avalonia.iOS/AutomationPeerWrapper.cs
new file mode 100644
index 00000000000..ad4c6e2c9b8
--- /dev/null
+++ b/src/iOS/Avalonia.iOS/AutomationPeerWrapper.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace Avalonia.iOS
+{
+ internal class AutomationPeerWrapper : UIAccessibilityElement
+ {
+ private static readonly IReadOnlyDictionary> s_propertySetters =
+ new Dictionary>()
+ {
+ { AutomationElementIdentifiers.NameProperty, UpdateName },
+ { AutomationElementIdentifiers.HelpTextProperty, UpdateHelpText },
+ { AutomationElementIdentifiers.BoundingRectangleProperty, UpdateBoundingRectangle },
+
+ { RangeValuePatternIdentifiers.IsReadOnlyProperty, UpdateIsReadOnly },
+ { RangeValuePatternIdentifiers.ValueProperty, UpdateValue },
+
+ { ValuePatternIdentifiers.IsReadOnlyProperty, UpdateIsReadOnly },
+ { ValuePatternIdentifiers.ValueProperty, UpdateValue },
+ };
+
+ private readonly AvaloniaView _view;
+ private readonly AutomationPeer _peer;
+
+ public AutomationPeerWrapper(AvaloniaView view, AutomationPeer? peer = null) : base(view)
+ {
+ _view = view;
+ _peer = peer ?? ControlAutomationPeer.CreatePeerForElement(view.TopLevel);
+
+ _peer.PropertyChanged += PeerPropertyChanged;
+ _peer.ChildrenChanged += PeerChildrenChanged;
+
+ AccessibilityContainer = _view;
+ AccessibilityIdentifier = _peer.GetAutomationId();
+ }
+
+ private static void UpdateName(AutomationPeerWrapper self)
+ {
+ AutomationPeer peer = self;
+ self.AccessibilityLabel = peer.GetName();
+ }
+
+ private static void UpdateHelpText(AutomationPeerWrapper self)
+ {
+ AutomationPeer peer = self;
+ self.AccessibilityHint = peer.GetHelpText();
+ }
+
+ private static void UpdateBoundingRectangle(AutomationPeerWrapper self)
+ {
+ AutomationPeer peer = self;
+ Rect bounds = peer.GetBoundingRectangle();
+ PixelRect screenRect = new PixelRect(
+ self._view.TopLevel.PointToScreen(bounds.TopLeft),
+ self._view.TopLevel.PointToScreen(bounds.BottomRight)
+ );
+ CGRect nativeRect = new CGRect(
+ screenRect.X, screenRect.Y,
+ screenRect.Width, screenRect.Height
+ );
+ if (self.AccessibilityFrame != nativeRect)
+ {
+ self.AccessibilityFrame = nativeRect;
+ UIAccessibility.PostNotification(UIAccessibilityPostNotification.LayoutChanged, null);
+ }
+ }
+
+ private static void UpdateIsReadOnly(AutomationPeerWrapper self)
+ {
+ AutomationPeer peer = self;
+ self.AccessibilityRespondsToUserInteraction =
+ peer.GetProvider()?.IsReadOnly ??
+ peer.GetProvider()?.IsReadOnly ??
+ peer.IsEnabled();
+ }
+
+ private static void UpdateValue(AutomationPeerWrapper self)
+ {
+ AutomationPeer peer = self;
+ string? newValue =
+ peer.GetProvider()?.Value.ToString("0.##") ??
+ peer.GetProvider()?.Value;
+ if (self.AccessibilityValue != newValue)
+ {
+ self.AccessibilityValue = newValue;
+ UIAccessibility.PostNotification(UIAccessibilityPostNotification.Announcement, null);
+ }
+ }
+
+ private void PeerChildrenChanged(object? sender, EventArgs e)
+ {
+ _view.UpdateChildren(_peer);
+ UIAccessibility.PostNotification(UIAccessibilityPostNotification.ScreenChanged, null);
+ }
+
+ private void PeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) =>
+ UpdateProperties(e.Property);
+
+ private void UpdateProperties(params AutomationProperty[] properties)
+ {
+ HashSet> calledSetters = new();
+ foreach (AutomationProperty property in properties)
+ {
+ if (s_propertySetters.TryGetValue(property,
+ out Action? setter) &&
+ !calledSetters.Contains(setter))
+ {
+ calledSetters.Add(setter);
+ setter.Invoke(this);
+ }
+ }
+ }
+
+ public bool UpdatePropertiesIfValid()
+ {
+ if (_peer.IsContentElement() && !_peer.IsOffscreen())
+ {
+ UpdateProperties(s_propertySetters.Keys.ToArray());
+ return IsAccessibilityElement = true;
+ }
+ else
+ {
+ return IsAccessibilityElement = false;
+ }
+ }
+
+ public void UpdateTraits()
+ {
+ UIAccessibilityTrait traits = UIAccessibilityTrait.None;
+
+ switch (_peer.GetAutomationControlType())
+ {
+ case AutomationControlType.Button:
+ traits |= UIAccessibilityTrait.Button;
+ break;
+ case AutomationControlType.Header:
+ traits |= UIAccessibilityTrait.Header;
+ break;
+ case AutomationControlType.Hyperlink:
+ traits |= UIAccessibilityTrait.Link;
+ break;
+ case AutomationControlType.Image:
+ traits |= UIAccessibilityTrait.Image;
+ break;
+ }
+
+ if (_peer.GetProvider()?.IsReadOnly == false)
+ {
+ traits |= UIAccessibilityTrait.Adjustable;
+ }
+
+ if (_peer.GetProvider()?.IsSelected == true)
+ {
+ traits |= UIAccessibilityTrait.Selected;
+ }
+
+ if (_peer.GetProvider()?.IsReadOnly == false)
+ {
+ traits |= UIAccessibilityTrait.UpdatesFrequently;
+ }
+
+ if (_peer.IsEnabled() == false)
+ {
+ traits |= UIAccessibilityTrait.NotEnabled;
+ }
+
+ AccessibilityTraits = (ulong)traits;
+ }
+
+ [Export("accessibilityActivate")]
+ public bool AccessibilityActivate()
+ {
+ IToggleProvider? toggleProvider = _peer.GetProvider();
+ IInvokeProvider? invokeProvider = _peer.GetProvider();
+ if (toggleProvider is not null)
+ {
+ toggleProvider.Toggle();
+ return true;
+ }
+ else if (invokeProvider is not null)
+ {
+ invokeProvider.Invoke();
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public override bool AccessibilityElementIsFocused()
+ {
+ base.AccessibilityElementIsFocused();
+ return _peer.HasKeyboardFocus();
+ }
+
+ public override void AccessibilityElementDidBecomeFocused()
+ {
+ base.AccessibilityElementDidBecomeFocused();
+ _peer.BringIntoView();
+ }
+
+ public override void AccessibilityDecrement()
+ {
+ base.AccessibilityDecrement();
+ IRangeValueProvider? provider = _peer.GetProvider();
+ if (provider is not null)
+ {
+ double value = provider.Value;
+ provider.SetValue(value - provider.SmallChange);
+ }
+ }
+
+ public override void AccessibilityIncrement()
+ {
+ base.AccessibilityIncrement();
+ IRangeValueProvider? provider = _peer.GetProvider();
+ if (provider is not null)
+ {
+ double value = provider.Value;
+ provider.SetValue(value + provider.SmallChange);
+ }
+ }
+
+ public override bool AccessibilityScroll(UIAccessibilityScrollDirection direction)
+ {
+ base.AccessibilityScroll(direction);
+ IScrollProvider? scrollProvider = _peer.GetProvider();
+ if (scrollProvider is not null)
+ {
+ bool didScroll;
+ ScrollAmount verticalAmount, horizontalAmount;
+ switch (direction)
+ {
+ case UIAccessibilityScrollDirection.Up:
+ verticalAmount = ScrollAmount.SmallIncrement;
+ horizontalAmount = ScrollAmount.NoAmount;
+ didScroll = true;
+ break;
+ case UIAccessibilityScrollDirection.Down:
+ verticalAmount = ScrollAmount.SmallDecrement;
+ horizontalAmount = ScrollAmount.NoAmount;
+ didScroll = true;
+ break;
+ case UIAccessibilityScrollDirection.Left:
+ verticalAmount = ScrollAmount.NoAmount;
+ horizontalAmount = ScrollAmount.SmallIncrement;
+ didScroll = true;
+ break;
+ case UIAccessibilityScrollDirection.Right:
+ verticalAmount = ScrollAmount.NoAmount;
+ horizontalAmount = ScrollAmount.SmallDecrement;
+ didScroll = true;
+ break;
+ default:
+ verticalAmount = ScrollAmount.NoAmount;
+ horizontalAmount = ScrollAmount.NoAmount;
+ didScroll = false;
+ break;
+ }
+
+ scrollProvider.Scroll(verticalAmount, horizontalAmount);
+ if (didScroll)
+ {
+ UIAccessibility.PostNotification(UIAccessibilityPostNotification.PageScrolled, null);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static implicit operator AutomationPeer(AutomationPeerWrapper instance)
+ {
+ return instance._peer;
+ }
+ }
+}
diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Automation.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Automation.cs
new file mode 100644
index 00000000000..717b1368b6e
--- /dev/null
+++ b/src/iOS/Avalonia.iOS/AvaloniaView.Automation.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Automation.Peers;
+using Foundation;
+using UIKit;
+
+namespace Avalonia.iOS
+{
+ public partial class AvaloniaView : IUIAccessibilityContainer
+ {
+ private readonly List _childrenList = new();
+ private readonly Dictionary _childrenMap = new();
+
+ [Export("accessibilityContainerType")]
+ public UIAccessibilityContainerType AccessibilityContainerType { get; set; } =
+ UIAccessibilityContainerType.SemanticGroup;
+
+ [Export("accessibilityElementCount")]
+ public nint AccessibilityElementCount()
+ {
+ UpdateChildren(_accessWrapper);
+ return _childrenList.Count;
+ }
+
+ [Export("accessibilityElementAtIndex:")]
+ public NSObject? GetAccessibilityElementAt(nint index)
+ {
+ try
+ {
+ var wrapper = _childrenMap[_childrenList[(int)index]];
+ if (wrapper.UpdatePropertiesIfValid())
+ {
+ return wrapper;
+ }
+ else
+ {
+ _childrenList.Remove(wrapper);
+ _childrenMap.Remove(wrapper);
+ }
+ }
+ catch (KeyNotFoundException) { }
+ catch (ArgumentOutOfRangeException) { }
+
+ return null;
+ }
+
+ [Export("indexOfAccessibilityElement:")]
+ public nint GetIndexOfAccessibilityElement(NSObject element)
+ {
+ int indexOf = _childrenList.IndexOf((AutomationPeerWrapper)element);
+ return indexOf < 0 ? NSRange.NotFound : indexOf;
+ }
+
+ internal void UpdateChildren(AutomationPeer peer)
+ {
+ foreach (AutomationPeer child in peer.GetChildren())
+ {
+ AutomationPeerWrapper? wrapper;
+ if (!_childrenMap.TryGetValue(child, out wrapper) &&
+ child.IsContentElement() && !child.IsOffscreen() &&
+ (child.GetName().Length > 0 || child.IsKeyboardFocusable()))
+ {
+ wrapper = new(this, child);
+ _childrenList.Add(child);
+ _childrenMap.Add(child, wrapper);
+ }
+
+ wrapper?.UpdatePropertiesIfValid();
+ wrapper?.UpdateTraits();
+
+ UpdateChildren(child);
+ }
+ }
+ }
+}
diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs
index 4248987d343..3d9148000fa 100644
--- a/src/iOS/Avalonia.iOS/AvaloniaView.cs
+++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs
@@ -31,9 +31,11 @@ internal IInputRoot InputRoot
=> _inputRoot ?? throw new InvalidOperationException($"{nameof(IWindowImpl.SetInputRoot)} must have been called");
internal TopLevel TopLevel => _topLevel;
+ private readonly AutomationPeerWrapper _accessWrapper;
private readonly TopLevelImpl _topLevelImpl;
private readonly EmbeddableControlRoot _topLevel;
private readonly InputHandler _input;
+
private TextInputMethodClient? _client;
private IAvaloniaViewController? _controller;
private IInputRoot? _inputRoot;
@@ -46,6 +48,9 @@ public AvaloniaView()
_input = new InputHandler(this, _topLevelImpl);
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
+ // Init accessibility tree root
+ _accessWrapper = new AutomationPeerWrapper(this);
+
_topLevel.Prepare();
_topLevel.StartRendering();
diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs
index aaf6b9992fd..4ff28ebf814 100644
--- a/tests/Avalonia.Base.UnitTests/VisualTests.cs
+++ b/tests/Avalonia.Base.UnitTests/VisualTests.cs
@@ -383,13 +383,13 @@ public void Removing_Child_Resets_IsEffectivelyVisible()
{
using var app = UnitTestApplication.Start();
var child = new Decorator();
- var root = new TestRoot { Child = child, IsVisible = false };
+ var root = new TestRoot { Child = child, IsVisible = true };
- Assert.False(child.IsEffectivelyVisible);
+ Assert.True(child.IsEffectivelyVisible);
root.Child = null;
- Assert.True(child.IsEffectivelyVisible);
+ Assert.False(child.IsEffectivelyVisible);
}
[Fact]
@@ -398,15 +398,15 @@ public void Removing_Child_Resets_IsEffectivelyVisible_Of_Grandchild()
using var app = UnitTestApplication.Start();
var grandchild = new Decorator();
var child = new Decorator { Child = grandchild };
- var root = new TestRoot { Child = child, IsVisible = false };
+ var root = new TestRoot { Child = child, IsVisible = true };
- Assert.False(child.IsEffectivelyVisible);
- Assert.False(grandchild.IsEffectivelyVisible);
+ Assert.True(child.IsEffectivelyVisible);
+ Assert.True(grandchild.IsEffectivelyVisible);
root.Child = null;
- Assert.True(child.IsEffectivelyVisible);
- Assert.True(grandchild.IsEffectivelyVisible);
+ Assert.False(child.IsEffectivelyVisible);
+ Assert.False(grandchild.IsEffectivelyVisible);
}
}
}