diff --git a/src/Avalonia.Controls.ColorPicker/Automation/Peers/ColorSpectrumAutomationPeer.cs b/src/Avalonia.Controls.ColorPicker/Automation/Peers/ColorSpectrumAutomationPeer.cs
new file mode 100644
index 0000000000..6e7e83a3a9
--- /dev/null
+++ b/src/Avalonia.Controls.ColorPicker/Automation/Peers/ColorSpectrumAutomationPeer.cs
@@ -0,0 +1,40 @@
+using Avalonia;
+using Avalonia.Automation;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Media;
+
+namespace Avalonia.Automation.Peers;
+
+public class ColorSpectrumAutomationPeer : ControlAutomationPeer, IValueProvider
+{
+ public ColorSpectrumAutomationPeer(ColorSpectrum owner)
+ : base(owner)
+ {
+ owner.ColorChanged += OwnerOnColorChanged;
+ }
+
+ public bool IsReadOnly => false;
+ public new ColorSpectrum Owner => (ColorSpectrum)base.Owner;
+ public string? Value => Owner.Color.ToString();
+
+ public void SetValue(string? value)
+ {
+ if (!Color.TryParse(value, out var color))
+ {
+ throw new System.FormatException($"Invalid color string: '{value}'.");
+ }
+
+ Owner.Color = color;
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom;
+
+ protected override string GetClassNameCore() => nameof(ColorSpectrum);
+
+ private void OwnerOnColorChanged(object? sender, ColorChangedEventArgs e)
+ {
+ RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, e.OldColor.ToString(), e.NewColor.ToString());
+ }
+}
diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
index 39cd5f0c5b..bd8d3fe65f 100644
--- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
+++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Collections.Pooled;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
@@ -197,6 +198,11 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromVisualTree(e);
}
+ protected override AutomationPeer OnCreateAutomationPeer()
+ {
+ return new ColorSpectrumAutomationPeer(this);
+ }
+
///
/// Explicitly unregisters all events connected in OnApplyTemplate().
///
@@ -397,6 +403,8 @@ namespace Avalonia.Controls.Primitives
{
if (change.Property == ColorProperty)
{
+ _oldColor = change.GetOldValue();
+
// If we're in the process of internally updating the color,
// then we don't want to respond to the Color property changing.
if (!_updatingColor)
@@ -410,9 +418,8 @@ namespace Avalonia.Controls.Primitives
UpdateEllipse();
UpdateBitmapSources();
+ RaiseColorChanged();
}
-
- _oldColor = change.GetOldValue();
}
else if (change.Property == HsvColorProperty)
{
diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
index fb5c959c90..f4132a5ced 100644
--- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
+++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs
@@ -15,6 +15,7 @@ using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls.Metadata;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
@@ -774,6 +775,8 @@ namespace Avalonia.Controls
FocusChanged(HasFocus());
}
+ protected override AutomationPeer OnCreateAutomationPeer() => new AutoCompleteBoxAutomationPeer(this);
+
///
/// Determines whether the text box or drop-down portion of the
/// control has
diff --git a/src/Avalonia.Controls/Automation/Peers/AutoCompleteBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutoCompleteBoxAutomationPeer.cs
new file mode 100644
index 0000000000..cf25070eae
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/AutoCompleteBoxAutomationPeer.cs
@@ -0,0 +1,68 @@
+using System;
+using Avalonia.Automation;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+ public class AutoCompleteBoxAutomationPeer : ControlAutomationPeer, IExpandCollapseProvider, IValueProvider
+ {
+ public AutoCompleteBoxAutomationPeer(AutoCompleteBox owner)
+ : base(owner)
+ {
+ owner.PropertyChanged += OwnerPropertyChanged;
+ }
+
+ public new AutoCompleteBox Owner => (AutoCompleteBox)base.Owner;
+
+ public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen);
+ public bool ShowsMenu => true;
+ public void Collapse() => Owner.IsDropDownOpen = false;
+ public void Expand() => Owner.IsDropDownOpen = true;
+ public bool IsReadOnly => false;
+
+ public string? Value
+ {
+ get => Owner.Text;
+ set
+ {
+ if (value == Owner.Text)
+ return;
+
+ Owner.Text = value;
+ }
+ }
+
+ void IValueProvider.SetValue(string? value) => Owner.Text = value;
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.Group;
+ }
+
+ protected override string GetClassNameCore() => nameof(AutoCompleteBox);
+
+ private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == AutoCompleteBox.IsDropDownOpenProperty)
+ {
+ RaisePropertyChangedEvent(
+ ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty,
+ ToState(e.GetOldValue()),
+ ToState(e.GetNewValue()));
+ }
+ else if (e.Property == AutoCompleteBox.TextProperty)
+ {
+ RaisePropertyChangedEvent(
+ ValuePatternIdentifiers.ValueProperty,
+ e.OldValue,
+ e.NewValue);
+ }
+ }
+
+ private static ExpandCollapseState ToState(bool value)
+ {
+ return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/CalendarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CalendarAutomationPeer.cs
new file mode 100644
index 0000000000..a79b1de1bc
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/CalendarAutomationPeer.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Automation;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+
+namespace Avalonia.Automation.Peers
+{
+ public class CalendarAutomationPeer : ControlAutomationPeer, ISelectionProvider, IValueProvider
+ {
+ public CalendarAutomationPeer(Calendar owner)
+ : base(owner)
+ {
+ owner.SelectedDatesChanged += OwnerSelectedDatesChanged;
+ }
+
+ public new Calendar Owner => (Calendar)base.Owner;
+
+ public bool CanSelectMultiple =>
+ Owner.SelectionMode == CalendarSelectionMode.SingleRange ||
+ Owner.SelectionMode == CalendarSelectionMode.MultipleRange;
+
+ public bool IsSelectionRequired => false;
+
+ public IReadOnlyList GetSelection()
+ {
+ return Owner.SelectedDates.Select(date => Owner.FindDayButtonFromDay(date))
+ .OfType().Select(GetOrCreate).ToList();
+ }
+
+ public bool IsReadOnly => true;
+
+ public string? Value => string.Join(
+ System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator,
+ Owner.SelectedDates.Select(x => x.ToString(System.Globalization.CultureInfo.CurrentCulture)));
+
+ public void SetValue(string? value)
+ {
+ throw new NotSupportedException();
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.Calendar;
+ }
+
+ protected override string GetClassNameCore() => nameof(Calendar);
+
+ private void OwnerSelectedDatesChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ RaisePropertyChangedEvent(SelectionPatternIdentifiers.SelectionProperty, null, null);
+ RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, null, null);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/CalendarDatePickerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CalendarDatePickerAutomationPeer.cs
new file mode 100644
index 0000000000..9fc8c49838
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/CalendarDatePickerAutomationPeer.cs
@@ -0,0 +1,72 @@
+using System;
+using Avalonia.Automation;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+ public class CalendarDatePickerAutomationPeer : ControlAutomationPeer,
+ IInvokeProvider,
+ IExpandCollapseProvider,
+ IValueProvider
+ {
+ public CalendarDatePickerAutomationPeer(CalendarDatePicker owner)
+ : base(owner)
+ {
+ Owner.PropertyChanged += OwnerPropertyChanged;
+ }
+
+ public new CalendarDatePicker Owner => (CalendarDatePicker)base.Owner;
+
+ public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen);
+
+ public bool ShowsMenu => true;
+
+ public bool IsReadOnly => false;
+
+ public string? Value => Owner.Text;
+
+ public void Invoke()
+ {
+ EnsureEnabled();
+ Owner.IsDropDownOpen = true;
+ }
+
+ public void Expand() => Owner.IsDropDownOpen = true;
+
+ public void Collapse() => Owner.IsDropDownOpen = false;
+
+ public void SetValue(string? value) => Owner.Text = value;
+
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Button;
+
+ protected override string GetClassNameCore() => "CalendarDatePicker";
+
+ protected override bool IsContentElementCore() => true;
+
+ protected override bool IsControlElementCore() => true;
+
+ private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == CalendarDatePicker.TextProperty)
+ {
+ RaisePropertyChangedEvent(
+ ValuePatternIdentifiers.ValueProperty,
+ e.OldValue,
+ e.NewValue);
+ }
+ else if (e.Property == CalendarDatePicker.IsDropDownOpenProperty)
+ {
+ RaisePropertyChangedEvent(
+ ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty,
+ ToState(e.GetOldValue()),
+ ToState(e.GetNewValue()));
+ }
+ }
+
+ private static ExpandCollapseState ToState(bool isExpanded)
+ {
+ return isExpanded ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/NativeMenuBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NativeMenuBarAutomationPeer.cs
new file mode 100644
index 0000000000..f39ba342fe
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/NativeMenuBarAutomationPeer.cs
@@ -0,0 +1,12 @@
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+ public class NativeMenuBarAutomationPeer(NativeMenuBar owner) : ControlAutomationPeer(owner)
+ {
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.MenuBar;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/NumericUpDownAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NumericUpDownAutomationPeer.cs
new file mode 100644
index 0000000000..3c205785af
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/NumericUpDownAutomationPeer.cs
@@ -0,0 +1,73 @@
+using System;
+using Avalonia.Automation;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+ public class NumericUpDownAutomationPeer : ControlAutomationPeer, IRangeValueProvider
+ {
+ public NumericUpDownAutomationPeer(NumericUpDown owner)
+ : base(owner)
+ {
+ Owner.PropertyChanged += OwnerPropertyChanged;
+ }
+
+ public new NumericUpDown Owner => (NumericUpDown)base.Owner;
+
+ public bool IsReadOnly => Owner.IsReadOnly;
+
+ public double Maximum => (double)Owner.Maximum;
+
+ public double Minimum => (double)Owner.Minimum;
+
+ public double Value => Owner.Value.HasValue
+ ? (double)Owner.Value.Value
+ : (double)Math.Clamp(0m, Owner.Minimum, Owner.Maximum);
+
+ public double SmallChange => (double)Owner.Increment;
+
+ public double LargeChange => (double)Owner.Increment;
+
+ public void SetValue(double value)
+ {
+ Owner.Value = (decimal)value;
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Spinner;
+
+ protected override string GetClassNameCore() => "NumericUpDown";
+
+ private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == NumericUpDown.MinimumProperty)
+ {
+ RaisePropertyChangedEvent(
+ RangeValuePatternIdentifiers.MinimumProperty,
+ e.OldValue,
+ e.NewValue);
+ }
+ else if (e.Property == NumericUpDown.MaximumProperty)
+ {
+ RaisePropertyChangedEvent(
+ RangeValuePatternIdentifiers.MaximumProperty,
+ e.OldValue,
+ e.NewValue);
+ }
+ else if (e.Property == NumericUpDown.ValueProperty)
+ {
+ RaisePropertyChangedEvent(
+ RangeValuePatternIdentifiers.ValueProperty,
+ e.OldValue,
+ e.NewValue);
+ }
+ else if (e.Property == NumericUpDown.IsReadOnlyProperty)
+ {
+ RaisePropertyChangedEvent(
+ RangeValuePatternIdentifiers.IsReadOnlyProperty,
+ e.OldValue,
+ e.NewValue);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/SplitButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SplitButtonAutomationPeer.cs
new file mode 100644
index 0000000000..a683ac1eb1
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/SplitButtonAutomationPeer.cs
@@ -0,0 +1,73 @@
+using Avalonia.Automation;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+ public class SplitButtonAutomationPeer : ContentControlAutomationPeer, IInvokeProvider, IExpandCollapseProvider
+ {
+ private ExpandCollapseState _expandCollapseState;
+
+ public SplitButtonAutomationPeer(SplitButton owner)
+ : base(owner)
+ {
+ _expandCollapseState = ToState(owner.IsFlyoutOpen);
+ owner.FlyoutStateChanged += OwnerFlyoutStateChanged;
+ }
+
+ public new SplitButton Owner => (SplitButton)base.Owner;
+
+ public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsFlyoutOpen);
+
+ public bool ShowsMenu => true;
+
+ public void Collapse()
+ {
+ Owner.CloseFlyoutForAutomation();
+ }
+
+ public void Expand()
+ {
+ Owner.OpenFlyoutForAutomation();
+ }
+
+ void IInvokeProvider.Invoke()
+ {
+ EnsureEnabled();
+ Owner.InvokePrimary();
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.SplitButton;
+ }
+
+ protected override string GetClassNameCore()
+ {
+ return "SplitButton";
+ }
+
+ protected override bool IsContentElementCore() => true;
+ protected override bool IsControlElementCore() => true;
+
+ private void OwnerFlyoutStateChanged(object? sender, System.EventArgs e)
+ {
+ var oldState = _expandCollapseState;
+ var newState = ToState(Owner.IsFlyoutOpen);
+
+ if (oldState != newState)
+ {
+ _expandCollapseState = newState;
+ RaisePropertyChangedEvent(
+ ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty,
+ oldState,
+ newState);
+ }
+ }
+
+ private static ExpandCollapseState ToState(bool isOpen)
+ {
+ return isOpen ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleSplitButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleSplitButtonAutomationPeer.cs
new file mode 100644
index 0000000000..7af8626e57
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/ToggleSplitButtonAutomationPeer.cs
@@ -0,0 +1,50 @@
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+ public class ToggleSplitButtonAutomationPeer : SplitButtonAutomationPeer, IToggleProvider
+ {
+ public ToggleSplitButtonAutomationPeer(ToggleSplitButton owner)
+ : base(owner)
+ {
+ owner.PropertyChanged += OwnerPropertyChanged;
+ }
+
+ public new ToggleSplitButton Owner => (ToggleSplitButton)base.Owner;
+
+ ToggleState IToggleProvider.ToggleState => ToState(Owner.IsChecked);
+
+ void IToggleProvider.Toggle()
+ {
+ EnsureEnabled();
+ Owner.ToggleForAutomation();
+ }
+
+ protected override AutomationControlType GetAutomationControlTypeCore()
+ {
+ return AutomationControlType.SplitButton;
+ }
+
+ protected override string GetClassNameCore()
+ {
+ return "ToggleSplitButton";
+ }
+
+ private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property == ToggleSplitButton.IsCheckedProperty)
+ {
+ RaisePropertyChangedEvent(
+ TogglePatternIdentifiers.ToggleStateProperty,
+ ToState(e.GetOldValue()),
+ ToState(e.GetNewValue()));
+ }
+ }
+
+ private static ToggleState ToState(bool value)
+ {
+ return value ? ToggleState.On : ToggleState.Off;
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/Automation/Peers/ToolTipAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToolTipAutomationPeer.cs
new file mode 100644
index 0000000000..f3d1f1a260
--- /dev/null
+++ b/src/Avalonia.Controls/Automation/Peers/ToolTipAutomationPeer.cs
@@ -0,0 +1,14 @@
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+ public class ToolTipAutomationPeer(ToolTip owner) : ControlAutomationPeer(owner)
+ {
+ public new ToolTip Owner => (ToolTip)base.Owner;
+
+ protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ToolTip;
+
+ protected override string GetClassNameCore() => "ToolTip";
+ }
+}
diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs
index 819019a634..7062a9af81 100644
--- a/src/Avalonia.Controls/Calendar/Calendar.cs
+++ b/src/Avalonia.Controls/Calendar/Calendar.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@@ -2251,5 +2252,6 @@ namespace Avalonia.Controls
}
}
+ protected override AutomationPeer OnCreateAutomationPeer() => new CalendarAutomationPeer(this);
}
}
diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
index 097fd2fb85..6194d4c14e 100644
--- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
+++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
+using Avalonia.Automation.Peers;
using Avalonia.Reactive;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
@@ -195,6 +196,8 @@ namespace Avalonia.Controls
UpdatePseudoClasses();
}
+ protected override AutomationPeer OnCreateAutomationPeer() => new CalendarDatePickerAutomationPeer(this);
+
///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs
index 022511bafa..ddb0b8136e 100644
--- a/src/Avalonia.Controls/NativeMenuBar.cs
+++ b/src/Avalonia.Controls/NativeMenuBar.cs
@@ -1,4 +1,5 @@
using System;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@@ -70,5 +71,7 @@ namespace Avalonia.Controls
menu.Bind(ItemsControl.ItemsSourceProperty, topLevel.GetBindingObservable(NativeMenu.MenuProperty)
.Select(v => v.GetValueOrDefault()?.Items)));
}
+
+ protected override AutomationPeer OnCreateAutomationPeer() => new NativeMenuBarAutomationPeer(this);
}
}
diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
index 073a2f109a..86a96ae7ba 100644
--- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
+++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
@@ -2,6 +2,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@@ -488,6 +489,8 @@ namespace Avalonia.Controls
SetValidSpinDirection();
}
+ protected override AutomationPeer OnCreateAutomationPeer() => new NumericUpDownAutomationPeer(this);
+
///
protected override void OnKeyDown(KeyEventArgs e)
{
diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs
index 390f679191..145df37e60 100644
--- a/src/Avalonia.Controls/SplitButton/SplitButton.cs
+++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs
@@ -1,6 +1,7 @@
using System;
using System.Windows.Input;
using Avalonia.Controls.Metadata;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -74,6 +75,8 @@ namespace Avalonia.Controls
private IDisposable? _flyoutPropertyChangedDisposable;
+ internal event EventHandler? FlyoutStateChanged;
+
///
/// Initializes a new instance of the class.
///
@@ -256,6 +259,8 @@ namespace Avalonia.Controls
UpdatePseudoClasses();
}
+ protected override AutomationPeer OnCreateAutomationPeer() => new SplitButtonAutomationPeer(this);
+
///
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
@@ -418,6 +423,23 @@ namespace Avalonia.Controls
}
}
+ internal void InvokePrimary()
+ {
+ OnClickPrimary(null);
+ }
+
+ internal bool IsFlyoutOpen => _isFlyoutOpen;
+
+ internal void OpenFlyoutForAutomation()
+ {
+ OpenFlyout();
+ }
+
+ internal void CloseFlyoutForAutomation()
+ {
+ CloseFlyout();
+ }
+
///
/// Invoked when the secondary button part is clicked.
///
@@ -516,6 +538,7 @@ namespace Avalonia.Controls
{
_isFlyoutOpen = true;
UpdatePseudoClasses();
+ FlyoutStateChanged?.Invoke(this, EventArgs.Empty);
OnFlyoutOpened();
}
@@ -533,6 +556,7 @@ namespace Avalonia.Controls
{
_isFlyoutOpen = false;
UpdatePseudoClasses();
+ FlyoutStateChanged?.Invoke(this, EventArgs.Empty);
OnFlyoutClosed();
}
diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs
index c493445ba1..3bd56f69f5 100644
--- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs
+++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs
@@ -1,5 +1,6 @@
using System;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@@ -65,6 +66,8 @@ namespace Avalonia.Controls
///
protected override Type StyleKeyOverride => typeof(SplitButton);
+ protected override AutomationPeer OnCreateAutomationPeer() => new ToggleSplitButtonAutomationPeer(this);
+
///
/// Toggles the property between true and false.
///
@@ -73,6 +76,11 @@ namespace Avalonia.Controls
SetCurrentValue(IsCheckedProperty, !IsChecked);
}
+ internal void ToggleForAutomation()
+ {
+ OnClickPrimary(null);
+ }
+
///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index d2b3da81fe..433098e3f5 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using Avalonia.Automation.Peers;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
@@ -472,5 +473,8 @@ namespace Avalonia.Controls
{
PseudoClasses.Set(":open", newValue);
}
+
+ protected override AutomationPeer OnCreateAutomationPeer()
+ => new ToolTipAutomationPeer(this);
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ColorSpectrumAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ColorSpectrumAutomationPeerTests.cs
new file mode 100644
index 0000000000..58c5b467a8
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Automation/ColorSpectrumAutomationPeerTests.cs
@@ -0,0 +1,91 @@
+using System;
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls.Primitives;
+using Avalonia.Media;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Automation;
+
+public class ColorSpectrumAutomationPeerTests
+{
+ public class AutomationPeerTests : ScopedTestBase
+ {
+ [Fact]
+ public void Creates_ColorSpectrumAutomationPeer()
+ {
+ var spectrum = new ColorSpectrum();
+ var peer = ControlAutomationPeer.CreatePeerForElement(spectrum);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void ControlType_Is_Custom()
+ {
+ var spectrum = new ColorSpectrum();
+ var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum);
+
+ Assert.Equal(AutomationControlType.Custom, peer.GetAutomationControlType());
+ }
+
+ [Fact]
+ public void ClassName_Is_ColorSpectrum()
+ {
+ var spectrum = new ColorSpectrum();
+ var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum);
+
+ Assert.Equal("ColorSpectrum", peer.GetClassName());
+ }
+
+ [Fact]
+ public void Implements_IValueProvider()
+ {
+ var spectrum = new ColorSpectrum();
+ var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum);
+
+ Assert.Equal(Colors.White.ToString(), peer.Value);
+
+ var valueProvider = Assert.IsAssignableFrom(peer);
+ valueProvider.SetValue("#00FF00");
+
+ Assert.Equal(Colors.Lime.ToString(), peer.Value);
+ Assert.Equal(Colors.Lime, spectrum.Color);
+ }
+
+ [Fact]
+ public void SetValue_Uses_Color_Parse_And_Throws_FormatException_On_Invalid_Input()
+ {
+ var spectrum = new ColorSpectrum();
+ var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum);
+ var valueProvider = Assert.IsAssignableFrom(peer);
+
+ Assert.Throws(() => valueProvider.SetValue("not-a-color"));
+ }
+
+ [Fact]
+ public void ValueProperty_Raises_AutomationPropertyChangedEvent_On_Color_Change()
+ {
+ var spectrum = new ColorSpectrum();
+ var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum);
+ AutomationPropertyChangedEventArgs? changed = null;
+
+ peer.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == ValuePatternIdentifiers.ValueProperty)
+ {
+ changed = e;
+ }
+ };
+
+ spectrum.Color = Colors.Black;
+
+ Assert.NotNull(changed);
+ Assert.Equal(ValuePatternIdentifiers.ValueProperty, changed!.Property);
+ Assert.Equal(Colors.White.ToString(), changed.OldValue);
+ Assert.Equal(Colors.Black.ToString(), changed.NewValue);
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ComplexControlAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ComplexControlAutomationPeerTests.cs
new file mode 100644
index 0000000000..52f4845a0a
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Automation/ComplexControlAutomationPeerTests.cs
@@ -0,0 +1,292 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using Avalonia;
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Automation;
+
+public class AutoCompleteBoxAutomationPeerTests : ScopedTestBase
+{
+ [Fact]
+ public void Creates_AutoCompleteBoxAutomationPeer()
+ {
+ var target = new AutoCompleteBox();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void Implements_IExpandCollapse_And_IValue_Providers()
+ {
+ var target = new AutoCompleteBox();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsAssignableFrom(peer);
+ Assert.IsAssignableFrom(peer);
+ Assert.False(peer is IInvokeProvider);
+ }
+
+ [Fact]
+ public void ControlType_Is_Group_And_ClassName_Is_AutoCompleteBox()
+ {
+ var target = new AutoCompleteBox();
+ var peer = (AutoCompleteBoxAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal(AutomationControlType.Group, peer.GetAutomationControlType());
+ Assert.Equal(nameof(AutoCompleteBox), peer.GetClassName());
+ }
+
+ [Fact]
+ public void ExpandCollapse_Tracks_IsDropDownOpen()
+ {
+ var target = new AutoCompleteBox
+ {
+ ItemsSource = new[] { "alpha" },
+ Text = "a"
+ };
+ var peer = (IExpandCollapseProvider)ControlAutomationPeer.CreatePeerForElement(target);
+ Assert.True(peer.ShowsMenu);
+
+ target.IsDropDownOpen = false;
+ Assert.False(target.IsDropDownOpen);
+
+ peer.Expand();
+ Assert.True(target.IsDropDownOpen);
+ Assert.Equal(ExpandCollapseState.Expanded, peer.ExpandCollapseState);
+
+ peer.Collapse();
+ Assert.False(target.IsDropDownOpen);
+ Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState);
+ }
+
+ [Fact]
+ public void Value_Tracks_And_Sets_Text()
+ {
+ var target = new AutoCompleteBox { Text = "one" };
+ var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal("one", peer.Value);
+ peer.SetValue("two");
+ Assert.Equal("two", target.Text);
+ Assert.Equal("two", ((IValueProvider)peer).Value);
+ }
+
+ [Fact]
+ public void ValueProvider_IsMutable()
+ {
+ var target = new AutoCompleteBox();
+ var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.False(peer.IsReadOnly);
+ }
+
+ [Fact]
+ public void Property_Change_Events_Raise_For_DropDown_And_Text()
+ {
+ var target = new AutoCompleteBox();
+ var peer = (AutoCompleteBoxAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ AutomationPropertyChangedEventArgs? expandCollapseChanged = null;
+ AutomationPropertyChangedEventArgs? valueChanged = null;
+ peer.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty)
+ {
+ expandCollapseChanged = e;
+ }
+ else if (e.Property == ValuePatternIdentifiers.ValueProperty)
+ {
+ valueChanged = e;
+ }
+ };
+
+ target.IsDropDownOpen = true;
+ Assert.NotNull(expandCollapseChanged);
+ Assert.Equal(ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, expandCollapseChanged!.Property);
+ Assert.Equal(ExpandCollapseState.Collapsed, expandCollapseChanged.OldValue);
+ Assert.Equal(ExpandCollapseState.Expanded, expandCollapseChanged.NewValue);
+
+ target.Text = "query";
+ Assert.NotNull(valueChanged);
+ Assert.Equal(ValuePatternIdentifiers.ValueProperty, valueChanged!.Property);
+ Assert.Equal(string.Empty, valueChanged.OldValue);
+ Assert.Equal("query", valueChanged.NewValue);
+ }
+}
+
+public class CalendarAutomationPeerTests : ScopedTestBase
+{
+ [Fact]
+ public void Creates_CalendarAutomationPeer()
+ {
+ var target = new Calendar();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void Implements_ISelection_And_IValue_Providers()
+ {
+ var target = new Calendar();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsAssignableFrom(peer);
+ Assert.IsAssignableFrom(peer);
+ }
+
+ [Fact]
+ public void ControlType_Is_Calendar_And_ClassName_Is_Calendar()
+ {
+ var target = new Calendar();
+ var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal(AutomationControlType.Calendar, peer.GetAutomationControlType());
+ Assert.Equal(nameof(Calendar), peer.GetClassName());
+ }
+
+ [Theory]
+ [InlineData(CalendarSelectionMode.SingleDate, false)]
+ [InlineData(CalendarSelectionMode.SingleRange, true)]
+ [InlineData(CalendarSelectionMode.MultipleRange, true)]
+ [InlineData(CalendarSelectionMode.None, false)]
+ public void CanSelectMultiple_Reflects_SelectionMode(CalendarSelectionMode selectionMode, bool canSelectMultiple)
+ {
+ var target = new Calendar { SelectionMode = selectionMode };
+ var peer = (ISelectionProvider)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal(canSelectMultiple, peer.CanSelectMultiple);
+ Assert.False(peer.IsSelectionRequired);
+ }
+
+ [Fact]
+ public void Selection_Events_Include_Selection_And_Value_Properties()
+ {
+ var target = new Calendar { SelectionMode = CalendarSelectionMode.SingleDate };
+ var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ AutomationPropertyChangedEventArgs? selectionChanged = null;
+ AutomationPropertyChangedEventArgs? valueChanged = null;
+ peer.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == SelectionPatternIdentifiers.SelectionProperty)
+ {
+ selectionChanged = e;
+ }
+ else if (e.Property == ValuePatternIdentifiers.ValueProperty)
+ {
+ valueChanged = e;
+ }
+ };
+
+ target.SelectedDate = new DateTime(2010, 1, 1);
+
+ Assert.NotNull(selectionChanged);
+ Assert.NotNull(valueChanged);
+ Assert.Equal(SelectionPatternIdentifiers.SelectionProperty, selectionChanged!.Property);
+ Assert.Equal(ValuePatternIdentifiers.ValueProperty, valueChanged!.Property);
+ }
+
+ [Fact]
+ public void Value_Joins_Selected_Dates_With_CurrentCulture()
+ {
+ var previousCulture = CultureInfo.CurrentCulture;
+ try
+ {
+ CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");
+
+ var selectedDates = new[] { new DateTime(2026, 5, 7), new DateTime(2026, 5, 8) };
+ var target = new Calendar
+ {
+ SelectionMode = CalendarSelectionMode.MultipleRange
+ };
+ var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ foreach (var date in selectedDates)
+ {
+ target.SelectedDates.Add(date);
+ }
+
+ Assert.Equal(
+ string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator, selectedDates.Select(x => x.ToString(CultureInfo.CurrentCulture))),
+ peer.Value);
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = previousCulture;
+ }
+ }
+
+ [Fact]
+ public void SetValue_Throws_NotSupported()
+ {
+ var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(new Calendar());
+
+ Assert.Throws(() => peer.SetValue("2026-01-01"));
+ }
+
+ [Fact]
+ public void Value_Is_ReadOnly()
+ {
+ var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(new Calendar());
+
+ Assert.True(peer.IsReadOnly);
+ }
+
+ [Fact]
+ public void GetSelection_Returns_Empty_When_DayButton_Not_Realized()
+ {
+ var target = new Calendar { SelectionMode = CalendarSelectionMode.SingleDate, SelectedDate = new DateTime(2026, 5, 7) };
+ var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ var selection = peer.GetSelection();
+
+ Assert.Empty(selection);
+ }
+
+ [Fact]
+ public void GetSelection_Returns_Realized_DayButton_Peers()
+ {
+ var selectedDate = new DateTime(2026, 5, 7);
+ var target = new Calendar
+ {
+ SelectionMode = CalendarSelectionMode.SingleDate,
+ DisplayDate = new DateTime(2026, 5, 1),
+ SelectedDate = selectedDate,
+ };
+ var monthView = new Grid();
+ var calendarItem = new CalendarItem
+ {
+ Owner = target,
+ MonthView = monthView
+ };
+ target.Root = new Panel { Children = { calendarItem } };
+
+ for (var i = 0; i < Calendar.ColumnsPerMonth; i++)
+ {
+ monthView.Children.Add(new TextBlock());
+ }
+
+ for (var i = 0; i < Calendar.RowsPerMonth * Calendar.ColumnsPerMonth - Calendar.ColumnsPerMonth; i++)
+ {
+ monthView.Children.Add(new CalendarDayButton
+ {
+ Owner = target,
+ DataContext = i == 0 ? selectedDate : selectedDate.AddDays(i + 1)
+ });
+ }
+
+ var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+ var selection = peer.GetSelection();
+
+ Assert.Single(selection);
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Automation/FoundationAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/FoundationAutomationPeerTests.cs
new file mode 100644
index 0000000000..29865d20e4
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Automation/FoundationAutomationPeerTests.cs
@@ -0,0 +1,266 @@
+using System;
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.UnitTests;
+using Xunit;
+
+#nullable enable
+
+namespace Avalonia.Controls.UnitTests.Automation;
+
+public class FoundationAutomationPeerTests
+{
+ public class ToolTipPeer : ScopedTestBase
+ {
+ [Fact]
+ public void Creates_ToolTipAutomationPeer()
+ {
+ var control = new ToolTip();
+ var peer = ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void ControlType_Is_ToolTip()
+ {
+ var control = new ToolTip();
+ var peer = (ToolTipAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.Equal(AutomationControlType.ToolTip, peer.GetAutomationControlType());
+ Assert.Equal("ToolTip", peer.GetClassName());
+ }
+ }
+
+ public class CalendarDatePickerPeer : ScopedTestBase
+ {
+ [Fact]
+ public void Creates_CalendarDatePickerAutomationPeer()
+ {
+ var control = new CalendarDatePicker();
+ var peer = ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void Implements_IInvoke_And_IValue_Providers()
+ {
+ var control = new CalendarDatePicker();
+ var peer = ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.IsAssignableFrom(peer);
+ Assert.IsAssignableFrom(peer);
+ Assert.IsAssignableFrom(peer);
+ }
+
+ [Fact]
+ public void Has_Button_ControlType()
+ {
+ var control = new CalendarDatePicker();
+ var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.Equal(AutomationControlType.Button, peer.GetAutomationControlType());
+ Assert.Equal("CalendarDatePicker", peer.GetClassName());
+ }
+
+ [Fact]
+ public void Invoke_Opens_DropDown()
+ {
+ var control = new CalendarDatePicker();
+ var peer = (IInvokeProvider)ControlAutomationPeer.CreatePeerForElement(control);
+
+ peer.Invoke();
+
+ Assert.True(control.IsDropDownOpen);
+ }
+
+ [Fact]
+ public void ExpandCollapse_Tracks_IsDropDownOpen()
+ {
+ var control = new CalendarDatePicker();
+ var peer = (IExpandCollapseProvider)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.True(peer.ShowsMenu);
+ Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState);
+
+ peer.Expand();
+ Assert.True(control.IsDropDownOpen);
+ Assert.Equal(ExpandCollapseState.Expanded, peer.ExpandCollapseState);
+
+ peer.Collapse();
+ Assert.False(control.IsDropDownOpen);
+ Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState);
+ }
+
+ [Fact]
+ public void Value_Mirrors_Owner_Text()
+ {
+ var control = new CalendarDatePicker { Text = "typed value" };
+ var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.Equal("typed value", peer.Value);
+
+ control.Text = "updated typed value";
+
+ Assert.Equal("updated typed value", peer.Value);
+ }
+
+ [Fact]
+ public void SetValue_Updates_Text()
+ {
+ var control = new CalendarDatePicker();
+ var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.False(peer.IsReadOnly);
+
+ peer.SetValue("automation text");
+
+ Assert.Equal("automation text", control.Text);
+ }
+
+ [Fact]
+ public void PropertyChanged_Raises_Value_When_Text_Changes()
+ {
+ var control = new CalendarDatePicker();
+ var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+ AutomationPropertyChangedEventArgs? changed = null;
+
+ peer.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == ValuePatternIdentifiers.ValueProperty)
+ changed = e;
+ };
+
+ control.Text = "January";
+
+ Assert.NotNull(changed);
+ Assert.Equal(ValuePatternIdentifiers.ValueProperty, changed!.Property);
+ Assert.Null(changed.OldValue);
+ Assert.Equal("January", changed.NewValue);
+ }
+
+ [Fact]
+ public void PropertyChanged_Raises_ExpandCollapseState_When_DropDown_Open_Changes()
+ {
+ var control = new CalendarDatePicker();
+ var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+ AutomationPropertyChangedEventArgs? changed = null;
+
+ peer.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty)
+ changed = e;
+ };
+
+ control.IsDropDownOpen = true;
+
+ Assert.NotNull(changed);
+ Assert.Equal(ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, changed!.Property);
+ Assert.Equal(ExpandCollapseState.Collapsed, changed.OldValue);
+ Assert.Equal(ExpandCollapseState.Expanded, changed.NewValue);
+ }
+ }
+
+ public class NumericUpDownPeer : ScopedTestBase
+ {
+ [Fact]
+ public void Creates_NumericUpDownAutomationPeer()
+ {
+ var control = new NumericUpDown();
+ var peer = ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void Is_Spinner_ControlType()
+ {
+ var control = new NumericUpDown();
+ var peer = (NumericUpDownAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.Equal(AutomationControlType.Spinner, peer.GetAutomationControlType());
+ Assert.Equal("NumericUpDown", peer.GetClassName());
+ }
+
+ [Fact]
+ public void Implements_IRangeValueProvider()
+ {
+ var control = new NumericUpDown();
+ var peer = ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.IsAssignableFrom(peer);
+ }
+
+ [Fact]
+ public void Range_Values_Reflect_Owner()
+ {
+ var control = new NumericUpDown
+ {
+ Minimum = 10,
+ Maximum = 20,
+ Increment = 3,
+ IsReadOnly = true,
+ Value = 14,
+ };
+
+ var peer = (IRangeValueProvider)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.Equal(10d, peer.Minimum);
+ Assert.Equal(20d, peer.Maximum);
+ Assert.Equal(14d, peer.Value);
+ Assert.Equal(3d, peer.SmallChange);
+ Assert.Equal(3d, peer.LargeChange);
+ Assert.True(peer.IsReadOnly);
+ }
+
+ [Fact]
+ public void Null_Value_Reports_Default_Clamped_To_Range()
+ {
+ var control = new NumericUpDown
+ {
+ Minimum = 10,
+ Maximum = 20,
+ Value = null,
+ };
+
+ var peer = (IRangeValueProvider)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.Equal(10d, peer.Value);
+ }
+
+ [Fact]
+ public void SetValue_Updates_Owner_Value()
+ {
+ var control = new NumericUpDown();
+ var peer = (IRangeValueProvider)ControlAutomationPeer.CreatePeerForElement(control);
+
+ peer.SetValue(42.5);
+
+ Assert.Equal(42.5m, control.Value);
+ }
+
+ [Fact]
+ public void PropertyChanged_Raises_Range_When_Value_Changes()
+ {
+ var control = new NumericUpDown();
+ var peer = (NumericUpDownAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+ AutomationPropertyChangedEventArgs? changed = null;
+
+ peer.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == RangeValuePatternIdentifiers.ValueProperty)
+ changed = e;
+ };
+
+ control.Value = 7.5m;
+
+ Assert.NotNull(changed);
+ Assert.Equal(RangeValuePatternIdentifiers.ValueProperty, changed!.Property);
+ Assert.Null(changed.OldValue);
+ Assert.Equal(7.5m, changed.NewValue);
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Automation/NativeMenuBarAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/NativeMenuBarAutomationPeerTests.cs
new file mode 100644
index 0000000000..4f4eb2a484
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Automation/NativeMenuBarAutomationPeerTests.cs
@@ -0,0 +1,46 @@
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Automation;
+
+public class NativeMenuBarAutomationPeerTests
+{
+ public class PeerCreation : ScopedTestBase
+ {
+ [Fact]
+ public void Creates_NativeMenuBarAutomationPeer()
+ {
+ var control = new NativeMenuBar { Template = CreateTemplate() };
+ var peer = ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void ControlType_Is_MenuBar()
+ {
+ var control = new NativeMenuBar { Template = CreateTemplate() };
+ var peer = (NativeMenuBarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control);
+
+ Assert.Equal(AutomationControlType.MenuBar, peer.GetAutomationControlType());
+ }
+ }
+
+ private static FuncControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate((_, ns) =>
+ new Menu
+ {
+ Name = "PART_NativeMenuPresenter",
+ Items =
+ {
+ new MenuItem { Header = "File" },
+ new MenuItem { Header = "Edit" },
+ },
+ }.RegisterInNameScope(ns));
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Automation/SplitButtonAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/SplitButtonAutomationPeerTests.cs
new file mode 100644
index 0000000000..9eb5e10ec9
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/Automation/SplitButtonAutomationPeerTests.cs
@@ -0,0 +1,196 @@
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Automation;
+
+public class SplitButtonAutomationPeerTests : ScopedTestBase
+{
+ [Fact]
+ public void Creates_SplitButtonAutomationPeer()
+ {
+ var target = new SplitButton();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void Implements_IExpandCollapseProvider()
+ {
+ var target = new SplitButton();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsAssignableFrom(peer);
+ }
+
+ [Fact]
+ public void Implements_IInvokeProvider()
+ {
+ var target = new SplitButton();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsAssignableFrom(peer);
+ }
+
+ [Fact]
+ public void ControlType_Is_SplitButton()
+ {
+ var target = new SplitButton();
+ var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal(AutomationControlType.SplitButton, peer.GetAutomationControlType());
+ }
+
+ [Fact]
+ public void ClassName_Is_SplitButton()
+ {
+ var target = new SplitButton();
+ var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal("SplitButton", peer.GetClassName());
+ }
+
+ [Fact]
+ public void ShowsMenu_Is_True()
+ {
+ var target = new SplitButton();
+ var peer = (IExpandCollapseProvider)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.True(peer.ShowsMenu);
+ }
+
+ [Fact]
+ public void Invoke_Triggers_Click()
+ {
+ var clicked = 0;
+ var target = new SplitButton();
+ var peer = (IInvokeProvider)ControlAutomationPeer.CreatePeerForElement(target);
+
+ target.Click += (_, _) => clicked++;
+ peer.Invoke();
+
+ Assert.Equal(1, clicked);
+ }
+
+ [Fact]
+ public void ExpandCollapse_State_Tracks_Flyout()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new SplitButton
+ {
+ Flyout = new Flyout()
+ };
+ var window = new Window
+ {
+ Content = target,
+ };
+ window.Show();
+
+ var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState);
+
+ peer.Expand();
+ Assert.Equal(ExpandCollapseState.Expanded, peer.ExpandCollapseState);
+ Assert.True(target.Flyout?.IsOpen);
+
+ peer.Collapse();
+ Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState);
+ Assert.False(target.Flyout!.IsOpen);
+ }
+ }
+
+ [Fact]
+ public void ExpandCollapse_Raises_PropertyChanged()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var target = new SplitButton
+ {
+ Flyout = new Flyout()
+ };
+ var window = new Window
+ {
+ Content = target,
+ };
+ window.Show();
+
+ var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+ AutomationPropertyChangedEventArgs? changed = null;
+ peer.PropertyChanged += (_, e) =>
+ {
+ if (e.Property == ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty)
+ {
+ changed = e;
+ }
+ };
+
+ peer.Expand();
+
+ Assert.NotNull(changed);
+ Assert.Equal(ExpandCollapseState.Collapsed, changed!.OldValue);
+ Assert.Equal(ExpandCollapseState.Expanded, changed.NewValue);
+ }
+ }
+}
+
+public class ToggleSplitButtonAutomationPeerTests : ScopedTestBase
+{
+ [Fact]
+ public void Creates_ToggleSplitButtonAutomationPeer()
+ {
+ var target = new ToggleSplitButton();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsType(peer);
+ }
+
+ [Fact]
+ public void Implements_IToggleProvider()
+ {
+ var target = new ToggleSplitButton();
+ var peer = ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.IsAssignableFrom(peer);
+ }
+
+ [Fact]
+ public void ControlType_Is_SplitButton()
+ {
+ var target = new ToggleSplitButton();
+ var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal(AutomationControlType.SplitButton, peer.GetAutomationControlType());
+ }
+
+ [Fact]
+ public void ClassName_Is_ToggleSplitButton()
+ {
+ var target = new ToggleSplitButton();
+ var peer = (ToggleSplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal("ToggleSplitButton", peer.GetClassName());
+ }
+
+ [Fact]
+ public void Toggle_Changes_IsChecked_And_Fires_Click()
+ {
+ var clicked = 0;
+ var target = new ToggleSplitButton();
+ var peer = (IToggleProvider)ControlAutomationPeer.CreatePeerForElement(target);
+
+ Assert.Equal(ToggleState.Off, peer.ToggleState);
+
+ target.Click += (_, _) => clicked++;
+ peer.Toggle();
+
+ Assert.True(target.IsChecked);
+ Assert.Equal(ToggleState.On, peer.ToggleState);
+ Assert.Equal(1, clicked);
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
index a92ba0dc83..4fbbd4c4ab 100644
--- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
+++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
@@ -21,6 +21,7 @@
+