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")); + } } }