diff --git a/src/Avalonia.Controls/Automation/Peers/CalendarDayButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CalendarDayButtonAutomationPeer.cs new file mode 100644 index 0000000000..49a56e7aea --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/CalendarDayButtonAutomationPeer.cs @@ -0,0 +1,78 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class CalendarDayButtonAutomationPeer : ButtonAutomationPeer, ISelectionItemProvider + { + public CalendarDayButtonAutomationPeer(CalendarDayButton owner) + : base(owner) + { + } + + public new CalendarDayButton Owner => (CalendarDayButton)base.Owner; + + public bool IsSelected => Owner.IsSelected; + + public ISelectionProvider? SelectionContainer + => Owner.Owner is { } calendar + ? GetOrCreate(calendar).GetProvider() + : null; + + public void Select() + { + EnsureEnabled(); + + if (TryGetSelectableDate(out var calendar, out var date)) + calendar.SelectedDate = date; + } + + void ISelectionItemProvider.AddToSelection() + { + EnsureEnabled(); + + if (!TryGetSelectableDate(out var calendar, out var date) || + calendar.SelectedDates.Contains(date)) + { + return; + } + + if (calendar.SelectionMode == CalendarSelectionMode.SingleDate) + calendar.SelectedDate = date; + else + calendar.SelectedDates.Add(date); + } + + void ISelectionItemProvider.RemoveFromSelection() + { + EnsureEnabled(); + + if (Owner.Owner is { SelectionMode: not CalendarSelectionMode.None } calendar && + Owner.DataContext is DateTime date) + { + calendar.SelectedDates.Remove(date); + } + } + + private bool TryGetSelectableDate([NotNullWhen(true)] out Calendar? calendar, out DateTime date) + { + if (Owner.Owner is { SelectionMode: not CalendarSelectionMode.None } owner && + Owner.DataContext is DateTime value && + !Owner.IsBlackout) + { + calendar = owner; + date = value; + return true; + } + + calendar = null; + date = default; + return false; + } + + protected override string GetClassNameCore() => nameof(CalendarDayButton); + } +} diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index c8556e444c..40a12e4ef5 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Input; @@ -157,6 +158,8 @@ namespace Avalonia.Controls.Primitives SetPseudoClasses(); } + protected override AutomationPeer OnCreateAutomationPeer() => new CalendarDayButtonAutomationPeer(this); + private void SetPseudoClasses() { if (_ignoringMouseOverState) diff --git a/tests/Avalonia.Controls.UnitTests/Automation/CalendarDayButtonAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/CalendarDayButtonAutomationPeerTests.cs new file mode 100644 index 0000000000..e4ac2de39a --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/CalendarDayButtonAutomationPeerTests.cs @@ -0,0 +1,166 @@ +using System; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Automation; + +public class CalendarDayButtonAutomationPeerTests : ScopedTestBase +{ + private static readonly DateTime Date1 = new(2026, 6, 5); + private static readonly DateTime Date2 = new(2026, 6, 10); + + private static (Calendar, CalendarDayButton, ISelectionItemProvider) CreateTarget( + CalendarSelectionMode mode, DateTime? date = null) + { + var calendar = new Calendar { SelectionMode = mode }; + var dayButton = new CalendarDayButton { Owner = calendar, DataContext = date ?? Date1 }; + var peer = ControlAutomationPeer.CreatePeerForElement(dayButton); + return (calendar, dayButton, Assert.IsAssignableFrom(peer)); + } + + [Fact] + public void Creates_CalendarDayButtonAutomationPeer() + { + var peer = ControlAutomationPeer.CreatePeerForElement(new CalendarDayButton()); + + Assert.IsType(peer); + Assert.IsAssignableFrom(peer); + Assert.IsAssignableFrom(peer); + } + + [Fact] + public void IsSelected_Reflects_Owner_State() + { + var (_, dayButton, provider) = CreateTarget(CalendarSelectionMode.SingleDate); + + Assert.False(provider.IsSelected); + dayButton.IsSelected = true; + Assert.True(provider.IsSelected); + } + + [Fact] + public void SelectionContainer_Is_Calendar_Peer() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.SingleDate); + + var container = provider.SelectionContainer; + + Assert.NotNull(container); + Assert.Same(ControlAutomationPeer.CreatePeerForElement(calendar), container); + } + + [Fact] + public void Select_Sets_SelectedDate() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.SingleDate); + + provider.Select(); + + Assert.Equal(Date1, calendar.SelectedDate); + } + + [Fact] + public void Select_Replaces_Existing_Selection_In_SingleDate_Mode() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.SingleDate); + calendar.SelectedDate = Date2; + + provider.Select(); + + Assert.Equal(Date1, calendar.SelectedDate); + Assert.Equal(1, calendar.SelectedDates.Count); + } + + [Fact] + public void Select_Is_NoOp_When_SelectionMode_None() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.None); + + provider.Select(); + + Assert.Null(calendar.SelectedDate); + Assert.Empty(calendar.SelectedDates); + } + + [Fact] + public void Select_Is_NoOp_For_Blackout_Day() + { + var (calendar, dayButton, provider) = CreateTarget(CalendarSelectionMode.SingleDate); + dayButton.IsBlackout = true; + + provider.Select(); + + Assert.Null(calendar.SelectedDate); + } + + [Fact] + public void AddToSelection_Replaces_Selection_In_SingleDate_Mode() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.SingleDate); + calendar.SelectedDate = Date2; + + provider.AddToSelection(); + + Assert.Equal(Date1, calendar.SelectedDate); + Assert.Equal(1, calendar.SelectedDates.Count); + } + + [Fact] + public void AddToSelection_Appends_In_MultipleRange_Mode() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.MultipleRange); + calendar.SelectedDates.Add(Date2); + + provider.AddToSelection(); + + Assert.Equal(2, calendar.SelectedDates.Count); + Assert.Contains(Date1, calendar.SelectedDates); + Assert.Contains(Date2, calendar.SelectedDates); + } + + [Fact] + public void AddToSelection_Is_NoOp_When_SelectionMode_None() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.None); + + provider.AddToSelection(); + + Assert.Empty(calendar.SelectedDates); + } + + [Fact] + public void AddToSelection_Is_NoOp_For_Blackout_Day() + { + var (calendar, dayButton, provider) = CreateTarget(CalendarSelectionMode.MultipleRange); + dayButton.IsBlackout = true; + + provider.AddToSelection(); + + Assert.Empty(calendar.SelectedDates); + } + + [Fact] + public void RemoveFromSelection_Removes_Date() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.MultipleRange); + calendar.SelectedDates.Add(Date1); + calendar.SelectedDates.Add(Date2); + + provider.RemoveFromSelection(); + + Assert.Equal(Date2, Assert.Single(calendar.SelectedDates)); + } + + [Fact] + public void RemoveFromSelection_Is_NoOp_When_SelectionMode_None() + { + var (calendar, _, provider) = CreateTarget(CalendarSelectionMode.None); + + provider.RemoveFromSelection(); + + Assert.Empty(calendar.SelectedDates); + } +}