diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml
index bd9f2a0d32..0ccca5d4cc 100644
--- a/samples/IntegrationTestApp/MainWindow.axaml
+++ b/samples/IntegrationTestApp/MainWindow.axaml
@@ -12,6 +12,10 @@
TextBlockWithNameAndAutomationId
+ Label for TextBox
+
+ Foo
+
diff --git a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
index aefd1febd0..8a30661359 100644
--- a/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
@@ -101,6 +101,12 @@ namespace Avalonia.Automation.Peers
///
public string GetClassName() => GetClassNameCore() ?? string.Empty;
+ ///
+ /// Gets the automation peer for the label that is targeted to the element.
+ ///
+ ///
+ public AutomationPeer? GetLabeledBy() => GetLabeledByCore();
+
///
/// Gets a human-readable localized string that represents the type of the control that is
/// associated with this automation peer.
@@ -207,6 +213,7 @@ namespace Avalonia.Automation.Peers
protected abstract Rect GetBoundingRectangleCore();
protected abstract IReadOnlyList GetOrCreateChildrenCore();
protected abstract string GetClassNameCore();
+ protected abstract AutomationPeer? GetLabeledByCore();
protected abstract string? GetNameCore();
protected abstract AutomationPeer? GetParentCore();
protected abstract bool HasKeyboardFocusCore();
diff --git a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
index 698bfd1460..721272c83e 100644
--- a/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
@@ -94,6 +94,7 @@ namespace Avalonia.Automation.Peers
protected override string? GetAutomationIdCore() => null;
protected override string GetClassNameCore() => typeof(ComboBoxItem).Name;
+ protected override AutomationPeer? GetLabeledByCore() => null;
protected override AutomationPeer? GetParentCore() => _owner;
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem;
diff --git a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
index a6e7a920e3..e68bf2ea47 100644
--- a/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
@@ -43,8 +43,24 @@ namespace Avalonia.Automation.Peers
}
protected override void BringIntoViewCore() => Owner.BringIntoView();
- protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name;
- protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds);
+
+ protected override IReadOnlyList GetOrCreateChildrenCore()
+ {
+ var children = _children ?? Array.Empty();
+
+ if (_childrenValid)
+ return children;
+
+ var newChildren = GetChildrenCore() ?? Array.Empty();
+
+ foreach (var peer in children.Except(newChildren))
+ peer.TrySetParent(null);
+ foreach (var peer in newChildren)
+ peer.TrySetParent(this);
+
+ _childrenValid = true;
+ return _children = newChildren;
+ }
protected virtual IReadOnlyList? GetChildrenCore()
{
@@ -66,6 +82,24 @@ namespace Avalonia.Automation.Peers
return result;
}
+ protected override AutomationPeer? GetLabeledByCore()
+ {
+ var label = AutomationProperties.GetLabeledBy(Owner);
+ return label is Control c ? GetOrCreatePeer(c) : null;
+ }
+
+ protected override string? GetNameCore()
+ {
+ var result = AutomationProperties.GetName(Owner);
+
+ if (string.IsNullOrWhiteSpace(result) && GetLabeledBy() is AutomationPeer labeledBy)
+ {
+ return labeledBy.GetName();
+ }
+
+ return null;
+ }
+
protected override AutomationPeer? GetParentCore()
{
EnsureConnected();
@@ -90,34 +124,6 @@ namespace Avalonia.Automation.Peers
_parentValid = false;
}
- protected override IReadOnlyList GetOrCreateChildrenCore()
- {
- var children = _children ?? Array.Empty();
-
- if (_childrenValid)
- return children;
-
- var newChildren = GetChildrenCore() ?? Array.Empty();
-
- foreach (var peer in children.Except(newChildren))
- peer.TrySetParent(null);
- foreach (var peer in newChildren)
- peer.TrySetParent(this);
-
- _childrenValid = true;
- return _children = newChildren;
- }
-
- protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom;
- protected override string GetClassNameCore() => Owner.GetType().Name;
- protected override string? GetNameCore() => AutomationProperties.GetName(Owner);
- protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
- protected override bool IsContentElementCore() => true;
- protected override bool IsControlElementCore() => true;
- protected override bool IsEnabledCore() => Owner.IsEnabled;
- protected override bool IsKeyboardFocusableCore() => Owner.Focusable;
- protected override void SetFocusCore() => Owner.Focus();
-
protected override bool ShowContextMenuCore()
{
var c = Owner;
@@ -142,6 +148,17 @@ namespace Avalonia.Automation.Peers
return true;
}
+ protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name;
+ protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds);
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom;
+ protected override string GetClassNameCore() => Owner.GetType().Name;
+ protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
+ protected override bool IsContentElementCore() => true;
+ protected override bool IsControlElementCore() => true;
+ protected override bool IsEnabledCore() => Owner.IsEnabled;
+ protected override bool IsKeyboardFocusableCore() => Owner.Focusable;
+ protected override void SetFocusCore() => Owner.Focus();
+
private Rect GetBounds(TransformedBounds? bounds)
{
return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default;
diff --git a/src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
similarity index 67%
rename from src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs
rename to src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
index c523feb0f8..a2ddedffc6 100644
--- a/src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
@@ -5,19 +5,21 @@ using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
- public class TextAutomationPeer : ControlAutomationPeer
+ public class TextBlockAutomationPeer : ControlAutomationPeer
{
- public TextAutomationPeer(IAutomationNodeFactory factory, Control owner)
+ public TextBlockAutomationPeer(IAutomationNodeFactory factory, TextBlock owner)
: base(factory, owner)
{
}
+ public new TextBlock Owner => (TextBlock)base.Owner;
+
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Text;
}
- protected override string? GetNameCore() => Owner.GetValue(TextBlock.TextProperty);
+ protected override string? GetNameCore() => Owner.Text;
protected override bool IsControlElementCore()
{
diff --git a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
index 8ee1aacb52..99f50ddabb 100644
--- a/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
+++ b/src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
@@ -6,7 +6,7 @@ using Avalonia.Controls;
namespace Avalonia.Automation.Peers
{
- public class TextBoxAutomationPeer : TextAutomationPeer, IValueProvider
+ public class TextBoxAutomationPeer : ControlAutomationPeer, IValueProvider
{
public TextBoxAutomationPeer(IAutomationNodeFactory factory, TextBox owner)
: base(factory, owner)
@@ -17,5 +17,10 @@ namespace Avalonia.Automation.Peers
public bool IsReadOnly => Owner.IsReadOnly;
public string? Value => Owner.Text;
public void SetValue(string? value) => Owner.Text = value;
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.Edit;
+ }
}
}
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index eb23878c3d..a80606988b 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -536,7 +536,7 @@ namespace Avalonia.Controls
protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
{
- return new TextAutomationPeer(factory, this);
+ return new TextBlockAutomationPeer(factory, this);
}
private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
diff --git a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs
index 3c6d9172c4..045a4f08d1 100644
--- a/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs
+++ b/tests/Avalonia.IntegrationTests.Win32/AutomationTests.cs
@@ -25,5 +25,15 @@ namespace Avalonia.IntegrationTests.Win32
var byName = _session.FindElementByAccessibilityId("TextBlockWithName");
var byAutomationId = _session.FindElementByAccessibilityId("TextBlockWithNameAndAutomationId");
}
+
+ [Fact]
+ public void LabeledBy()
+ {
+ var label = _session.FindElementByAccessibilityId("TextBlockAsLabel");
+ var labeledTextBox = _session.FindElementByAccessibilityId("LabeledByTextBox");
+
+ Assert.Equal("Label for TextBox", label.Text);
+ Assert.Equal("Label for TextBox", labeledTextBox.GetAttribute("Name"));
+ }
}
}