diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml
index cfae7140b0..e142c4da72 100644
--- a/samples/ControlCatalog/Pages/CalendarPage.xaml
+++ b/samples/ControlCatalog/Pages/CalendarPage.xaml
@@ -32,6 +32,11 @@
+
+
+
+
AllowTapRangeSelectionProperty =
+ AvaloniaProperty.Register(
+ nameof(AllowTapRangeSelection),
+ defaultValue: true);
+
///
/// Gets or sets a value that indicates what kind of selections are
/// allowed.
@@ -462,6 +469,24 @@ namespace Avalonia.Controls
set => SetValue(SelectionModeProperty, value);
}
+ ///
+ /// Gets or sets a value indicating whether tap-to-select range mode is enabled.
+ /// When enabled, users can tap a start date and then tap an end date to select a range.
+ ///
+ ///
+ /// True to enable tap range selection; otherwise, false. The default is false.
+ ///
+ ///
+ /// This feature only works when SelectionMode is set to SingleRange.
+ /// When enabled, the first tap selects the start date, and the second tap selects
+ /// the end date to complete the range. Tapping a third date starts a new range.
+ ///
+ public bool AllowTapRangeSelection
+ {
+ get => GetValue(AllowTapRangeSelectionProperty);
+ set => SetValue(AllowTapRangeSelectionProperty, value);
+ }
+
private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e)
{
if (IsValidSelectionMode(e.NewValue!))
@@ -470,6 +495,10 @@ namespace Avalonia.Controls
SetCurrentValue(SelectedDateProperty, null);
_displayDateIsChanging = false;
SelectedDates.Clear();
+
+ // Reset tap range selection state when mode changes
+ _isTapRangeSelectionActive = false;
+ _tapRangeStart = null;
}
else
{
@@ -477,6 +506,12 @@ namespace Avalonia.Controls
}
}
+ private void OnAllowTapRangeSelectionChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ _isTapRangeSelectionActive = false;
+ _tapRangeStart = null;
+ }
+
///
/// Inherited code: Requires comment.
///
@@ -1450,6 +1485,94 @@ namespace Avalonia.Controls
SelectedDates.AddRange(HoverStart.Value, HoverEnd.Value);
}
}
+
+ ///
+ /// Handles tap range selection logic for date range selection.
+ ///
+ /// The date that was tapped.
+ /// True if the tap was handled as part of range selection; otherwise, false.
+ internal bool ProcessTapRangeSelection(DateTime selectedDate)
+ {
+ if (!AllowTapRangeSelection ||
+ (SelectionMode != CalendarSelectionMode.SingleRange && SelectionMode != CalendarSelectionMode.MultipleRange))
+ {
+ return false;
+ }
+
+ if (!IsValidDateSelection(this, selectedDate))
+ {
+ return false;
+ }
+
+ if (!_isTapRangeSelectionActive || !_tapRangeStart.HasValue)
+ {
+ _isTapRangeSelectionActive = true;
+ _tapRangeStart = selectedDate;
+
+ if (SelectionMode == CalendarSelectionMode.SingleRange)
+ {
+ foreach (DateTime item in SelectedDates)
+ {
+ RemovedItems.Add(item);
+ }
+ SelectedDates.ClearInternal();
+ }
+
+ if (!SelectedDates.Contains(selectedDate))
+ {
+ SelectedDates.Add(selectedDate);
+ }
+
+ return true;
+ }
+ else
+ {
+ DateTime startDate = _tapRangeStart.Value;
+ DateTime endDate = selectedDate;
+
+ if (DateTime.Compare(startDate, endDate) > 0)
+ {
+ (startDate, endDate) = (endDate, startDate);
+ }
+
+ CalendarDateRange range = new CalendarDateRange(startDate, endDate);
+ if (BlackoutDates.ContainsAny(range))
+ {
+ _tapRangeStart = selectedDate;
+
+ if (SelectionMode == CalendarSelectionMode.SingleRange)
+ {
+ foreach (DateTime item in SelectedDates)
+ {
+ RemovedItems.Add(item);
+ }
+ SelectedDates.ClearInternal();
+ }
+
+ if (!SelectedDates.Contains(selectedDate))
+ {
+ SelectedDates.Add(selectedDate);
+ }
+ return true;
+ }
+
+ if (SelectionMode == CalendarSelectionMode.SingleRange)
+ {
+ foreach (DateTime item in SelectedDates)
+ {
+ RemovedItems.Add(item);
+ }
+ SelectedDates.ClearInternal();
+ }
+
+ SelectedDates.AddRange(startDate, endDate);
+
+ _isTapRangeSelectionActive = false;
+ _tapRangeStart = null;
+
+ return true;
+ }
+ }
private void ProcessSelection(bool shift, DateTime? lastSelectedDate, int? index)
{
if (SelectionMode == CalendarSelectionMode.None && lastSelectedDate != null)
@@ -1457,6 +1580,17 @@ namespace Avalonia.Controls
OnDayClick(lastSelectedDate.Value);
return;
}
+
+ // Handle tap range selection.
+ if (lastSelectedDate != null && index == null && !shift)
+ {
+ if (ProcessTapRangeSelection(lastSelectedDate.Value))
+ {
+ OnDayClick(lastSelectedDate.Value);
+ return;
+ }
+ }
+
if (lastSelectedDate != null && IsValidKeyboardSelection(this, lastSelectedDate.Value))
{
if (SelectionMode == CalendarSelectionMode.SingleRange || SelectionMode == CalendarSelectionMode.MultipleRange)
@@ -2069,6 +2203,7 @@ namespace Avalonia.Controls
IsTodayHighlightedProperty.Changed.AddClassHandler((x, e) => x.OnIsTodayHighlightedChanged(e));
DisplayModeProperty.Changed.AddClassHandler((x, e) => x.OnDisplayModePropertyChanged(e));
SelectionModeProperty.Changed.AddClassHandler((x, e) => x.OnSelectionModeChanged(e));
+ AllowTapRangeSelectionProperty.Changed.AddClassHandler((x, e) => x.OnAllowTapRangeSelectionChanged(e));
SelectedDateProperty.Changed.AddClassHandler((x, e) => x.OnSelectedDateChanged(e));
DisplayDateProperty.Changed.AddClassHandler((x, e) => x.OnDisplayDateChanged(e));
DisplayDateStartProperty.Changed.AddClassHandler((x, e) => x.OnDisplayDateStartChanged(e));
diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs
index c2801e6e64..2bfe3c9be2 100644
--- a/src/Avalonia.Controls/Calendar/CalendarItem.cs
+++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs
@@ -1030,6 +1030,15 @@ namespace Avalonia.Controls.Primitives
Owner.OnDayClick(selectedDate);
return;
}
+ if (Owner.AllowTapRangeSelection &&
+ (Owner.SelectionMode == CalendarSelectionMode.SingleRange || Owner.SelectionMode == CalendarSelectionMode.MultipleRange))
+ {
+ if (Owner.ProcessTapRangeSelection(selectedDate))
+ {
+ Owner.OnDayClick(selectedDate);
+ return;
+ }
+ }
if (Owner.HoverStart.HasValue)
{
switch (Owner.SelectionMode)
diff --git a/tests/Avalonia.Controls.UnitTests/CalendarTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarTests.cs
index 7f464827d7..864f92f511 100644
--- a/tests/Avalonia.Controls.UnitTests/CalendarTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/CalendarTests.cs
@@ -271,5 +271,97 @@ namespace Avalonia.Controls.UnitTests
Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(100)));
Assert.True(calendar.SelectedDates.Count == 1);
}
+
+ [Fact]
+ public void AllowTapRangeSelection_Should_Disable_TapToSelectRange()
+ {
+ var calendar = new Calendar();
+ Assert.True(calendar.AllowTapRangeSelection); // Default should be true
+
+ calendar.AllowTapRangeSelection = false;
+ Assert.False(calendar.AllowTapRangeSelection);
+ }
+
+ [Fact]
+ public void TapRangeSelection_Should_Work_In_SingleRange_Mode()
+ {
+ var calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleRange;
+ calendar.AllowTapRangeSelection = true;
+
+ var startDate = new DateTime(2023, 10, 10);
+ var endDate = new DateTime(2023, 10, 15);
+
+ // First tap should select start date
+ var firstTapResult = calendar.ProcessTapRangeSelection(startDate);
+ Assert.True(firstTapResult);
+ Assert.Equal(1, calendar.SelectedDates.Count);
+ Assert.True(calendar.SelectedDates.Contains(startDate));
+
+ // Second tap should complete the range
+ var secondTapResult = calendar.ProcessTapRangeSelection(endDate);
+ Assert.True(secondTapResult);
+ Assert.Equal(6, calendar.SelectedDates.Count); // 5 days inclusive
+ Assert.True(calendar.SelectedDates.Contains(startDate));
+ Assert.True(calendar.SelectedDates.Contains(endDate));
+ }
+
+ [Fact]
+ public void TapRangeSelection_Should_Not_Work_In_SingleDate_Mode()
+ {
+ var calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleDate;
+ calendar.AllowTapRangeSelection = true;
+
+ var date = new DateTime(2023, 10, 10);
+ var result = calendar.ProcessTapRangeSelection(date);
+ Assert.False(result); // Should not handle tap range selection
+ }
+
+ [Fact]
+ public void TapRangeSelection_Should_Handle_Blackout_Dates()
+ {
+ var calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleRange;
+ calendar.AllowTapRangeSelection = true;
+
+ var startDate = new DateTime(2023, 10, 10);
+ var blackoutDate = new DateTime(2023, 10, 12);
+ var endDate = new DateTime(2023, 10, 15);
+
+ // Add blackout date in the middle
+ calendar.BlackoutDates.Add(new CalendarDateRange(blackoutDate, blackoutDate));
+
+ // First tap
+ calendar.ProcessTapRangeSelection(startDate);
+ Assert.Equal(1, calendar.SelectedDates.Count);
+
+ // Second tap should restart selection due to blackout date
+ calendar.ProcessTapRangeSelection(endDate);
+ Assert.Equal(1, calendar.SelectedDates.Count);
+ Assert.True(calendar.SelectedDates.Contains(endDate));
+ Assert.False(calendar.SelectedDates.Contains(startDate));
+ }
+
+ [Fact]
+ public void TapRangeSelection_Should_Handle_Reverse_Order_Dates()
+ {
+ var calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleRange;
+ calendar.AllowTapRangeSelection = true;
+
+ var laterDate = new DateTime(2023, 10, 15);
+ var earlierDate = new DateTime(2023, 10, 10);
+
+ // First tap on later date
+ calendar.ProcessTapRangeSelection(laterDate);
+ Assert.Equal(1, calendar.SelectedDates.Count);
+
+ // Second tap on earlier date should still create correct range
+ calendar.ProcessTapRangeSelection(earlierDate);
+ Assert.Equal(6, calendar.SelectedDates.Count);
+ Assert.True(calendar.SelectedDates.Contains(earlierDate));
+ Assert.True(calendar.SelectedDates.Contains(laterDate));
+ }
}
}