Browse Source

Fix CalendarDatePicker becoming unusable when closed programmatically (#20756)

* Fix CalendarDatePicker becoming unusable when closed programmatically

* Add unit test for CalendarDatePicker
release/11.3.13
Evan 8 months ago
committed by Julien Lebosquain
parent
commit
d7a4b6157f
No known key found for this signature in database GPG Key ID: 1833CAD10ACC46FD
  1. 13
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  2. 102
      tests/Avalonia.Controls.UnitTests/CalendarTests.cs

13
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -299,6 +299,19 @@ namespace Avalonia.Controls.Primitives
}
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
// Reset mouse button tracking state. When the calendar popup closes
// (e.g. due to a programmatic window change during date selection),
// the PointerReleased event never fires, leaving these flags stuck.
// See https://github.com/AvaloniaUI/Avalonia/issues/18418
_isMouseLeftButtonDown = false;
_isMouseLeftButtonDownYearView = false;
}
private void SetDayTitles()
{
for (int childIndex = 0; childIndex < Calendar.ColumnsPerMonth; childIndex++)

102
tests/Avalonia.Controls.UnitTests/CalendarTests.cs

@ -1,12 +1,16 @@
using Xunit;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.UnitTests;
#nullable enable
namespace Avalonia.Controls.UnitTests
{
public class CalendarTests
public class CalendarTests : ScopedTestBase
{
private static bool CompareDates(DateTime first, DateTime second)
{
@ -142,12 +146,12 @@ namespace Avalonia.Controls.UnitTests
/// <summary>
/// The days added to the SelectedDates collection.
/// </summary>
private IList<object> _selectedDatesChangedAddedDays;
private IList<object>? _selectedDatesChangedAddedDays;
/// <summary>
/// The days removed from the SelectedDates collection.
/// </summary>
private IList<object> _selectedDateChangedRemovedDays;
private IList<object>? _selectedDatesChangedRemovedDays;
/// <summary>
/// The number of times the SelectedDatesChanged event has been fired.
@ -159,13 +163,13 @@ namespace Avalonia.Controls.UnitTests
/// </summary>
/// <param name="sender">The calendar.</param>
/// <param name="e">Event arguments.</param>
private void OnSelectedDatesChanged(object sender, SelectionChangedEventArgs e)
private void OnSelectedDatesChanged(object? sender, SelectionChangedEventArgs e)
{
_selectedDatesChangedAddedDays =
e.AddedItems
.Cast<object>()
.ToList();
_selectedDateChangedRemovedDays =
_selectedDatesChangedRemovedDays =
e.RemovedItems
.Cast<object>()
.ToList();
@ -182,9 +186,9 @@ namespace Avalonia.Controls.UnitTests
_selectedDatesChangedAddedDays.Clear();
}
if (_selectedDateChangedRemovedDays != null)
if (_selectedDatesChangedRemovedDays != null)
{
_selectedDateChangedRemovedDays.Clear();
_selectedDatesChangedRemovedDays.Clear();
}
_selectedDatesChangedCount = 0;
@ -202,8 +206,10 @@ namespace Avalonia.Controls.UnitTests
Assert.True(calendar.SelectedDates.Count == 1);
Assert.True(CompareDates(calendar.SelectedDates[0], DateTime.Today));
Assert.True(_selectedDatesChangedCount == 1);
Assert.NotNull(_selectedDatesChangedAddedDays);
Assert.True(_selectedDatesChangedAddedDays.Count == 1);
Assert.True(_selectedDateChangedRemovedDays.Count == 0);
Assert.NotNull(_selectedDatesChangedRemovedDays);
Assert.True(_selectedDatesChangedRemovedDays.Count == 0);
ResetSelectedDatesChanged();
calendar.SelectedDate = DateTime.Today;
@ -240,8 +246,10 @@ namespace Avalonia.Controls.UnitTests
Assert.True(calendar.SelectedDates.Count == 1);
Assert.True(CompareDates(calendar.SelectedDates[0], DateTime.Today));
Assert.True(_selectedDatesChangedCount == 1);
Assert.NotNull(_selectedDatesChangedAddedDays);
Assert.True(_selectedDatesChangedAddedDays.Count == 1);
Assert.True(_selectedDateChangedRemovedDays.Count == 0);
Assert.NotNull(_selectedDatesChangedRemovedDays);
Assert.True(_selectedDatesChangedRemovedDays.Count == 0);
ResetSelectedDatesChanged();
calendar.SelectedDates.Clear();
@ -263,7 +271,7 @@ namespace Avalonia.Controls.UnitTests
Assert.True(calendar.SelectedDates.Count == 21);
Assert.True(_selectedDatesChangedCount == 1);
Assert.True(_selectedDatesChangedAddedDays.Count == 21);
Assert.True(_selectedDateChangedRemovedDays.Count == 11);
Assert.True(_selectedDatesChangedRemovedDays.Count == 11);
ResetSelectedDatesChanged();
calendar.SelectedDates.Add(DateTime.Today.AddDays(100));
@ -362,5 +370,77 @@ namespace Avalonia.Controls.UnitTests
Assert.True(calendar.SelectedDates.Contains(earlierDate));
Assert.True(calendar.SelectedDates.Contains(laterDate));
}
[Fact]
public void CalendarItem_Should_Reset_Mouse_Down_Flag_On_Detach_From_Visual_Tree()
{
var calendar = new Calendar();
calendar.SelectionMode = CalendarSelectionMode.SingleDate;
var calendarItem = new CalendarItem();
calendarItem.Owner = calendar;
// Attach CalendarItem to a visual tree
var root = new TestRoot(calendarItem);
// Create a day button and simulate mouse left button down,
// which sets the internal _isMouseLeftButtonDown flag to true.
var date1 = new DateTime(2024, 1, 15);
var dayButton1 = new CalendarDayButton { DataContext = date1 };
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
var props = new PointerPointProperties(RawInputModifiers.LeftMouseButton,
PointerUpdateKind.LeftButtonPressed);
var pressArgs = new PointerPressedEventArgs(dayButton1, pointer, root,
default, 0, props, KeyModifiers.None);
calendarItem.Cell_MouseLeftButtonDown(dayButton1, pressArgs);
// date1 should now be selected
Assert.Equal(1, calendar.SelectedDates.Count);
Assert.Equal(date1, calendar.SelectedDates[0]);
// Detach CalendarItem from visual tree (simulates popup closing
// during date selection without a PointerReleased event).
root.Child = null;
// Create a different day button and simulate mouse enter.
// Before the fix, _isMouseLeftButtonDown would still be true,
// causing hover to auto-select dates.
var date2 = new DateTime(2024, 1, 20);
var dayButton2 = new CalendarDayButton { DataContext = date2 };
calendarItem.Cell_MouseEntered(dayButton2, null!);
// The selected date should NOT have changed to date2,
// because the mouse-down flag was reset when detaching.
Assert.Equal(1, calendar.SelectedDates.Count);
Assert.Equal(date1, calendar.SelectedDates[0]);
}
[Fact]
public void CalendarItem_Should_Reset_YearView_Mouse_Down_Flag_On_Detach_From_Visual_Tree()
{
var calendar = new Calendar();
var calendarItem = new CalendarItem();
calendarItem.Owner = calendar;
// Attach CalendarItem to a visual tree
var root = new TestRoot(calendarItem);
// Use reflection to set the _isMouseLeftButtonDownYearView flag,
// since Month_CalendarButtonMouseDown is private.
var field = typeof(CalendarItem).GetField("_isMouseLeftButtonDownYearView",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Assert.NotNull(field);
field!.SetValue(calendarItem, true);
Assert.True((bool)field.GetValue(calendarItem)!);
// Detach CalendarItem from visual tree
root.Child = null;
// Verify the flag was reset
Assert.False((bool)field.GetValue(calendarItem)!);
}
}
}

Loading…
Cancel
Save